@xano-sdk/chatbot 1.0.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/llms.txt ADDED
@@ -0,0 +1,487 @@
1
+ # @xano-sdk/chatbot
2
+
3
+ > A conversational AI assistant for Xano as typed Xano SDK defs: `conversation` +
4
+ > `conversation_message` tables, a Xano AI agent, a shared reply function, and the
5
+ > chat endpoints. Point it at your users table, write a system prompt, deploy.
6
+ > For agents working ON this repo, read AGENTS.md instead.
7
+
8
+ Install: `npm install @xano-sdk/chatbot @xano/sdk`. `@xano/sdk` is a
9
+ `>=1.0.0 <2.0.0` peer dependency; built and tested against **1.0.0**. ESM-only,
10
+ Node >= 20.
11
+
12
+ ## Setup
13
+
14
+ ```ts
15
+ import { workspace } from "@xano/sdk";
16
+ import { userTable } from "@xano-sdk/auth"; // or any table() handle
17
+ import { registerChatbot } from "@xano-sdk/chatbot";
18
+
19
+ export const bot = registerChatbot(workspace("my-app"), {
20
+ authTable: userTable, // any table() handle or table name
21
+ llm: { type: "xano-free", systemPrompt: "You are a support agent for Acme." },
22
+ });
23
+
24
+ export default bot.xano; // the default export must be the Xano registry
25
+ ```
26
+
27
+ - **`registerChatbot` returns the def set, not the instance.** The instance is on
28
+ `.xano`. The defs are factories, so this handle is the ONLY route to the
29
+ registered defs — exporting it is what lets a frontend derive its types from
30
+ them (see "Typed client" below). Use `createChatbot(opts)` to build without
31
+ registering.
32
+ - **The families are typed from the options — no `!` needed.**
33
+ `bot.authenticated` is non-nullable unless you pass `{ authenticated: false }`,
34
+ and `bot.guest` exists only with `{ guest: true }`:
35
+
36
+ ```ts
37
+ const a = registerChatbot(xano, { authTable }); // a.authenticated.sendMessage ✓ a.guest → undefined
38
+ const b = registerChatbot(xano, { authTable, guest: true }); // b.guest.sendMessage ✓
39
+ const c = registerChatbot(xano, { authenticated: false, guest: true }); // c.authenticated → undefined
40
+ ```
41
+
42
+ Only a NON-LITERAL flag (`{ guest: someBoolean }`) stays `… | undefined`,
43
+ because it is genuinely unknowable at compile time.
44
+
45
+ - **No dependency on `@xano-sdk/auth`.** `authTable` takes any `table()` handle or
46
+ bare table name. `table({ auth: true })` is a convention, not a mechanism — the
47
+ engine gates by comparing the token's table to the endpoint's *by name* and
48
+ mints a token for any table by name; the flag only drives the editor's picker.
49
+ - A bare-name `authTable` must still be **registered** on the workspace (the SDK
50
+ errors at export on an unresolvable reference) and needs
51
+ `{ userIdType: "uuid" }` for a uuid-keyed table, since a name carries no schema.
52
+ Prefer the handle.
53
+ - A raw numeric `dbo.id` is **rejected**, unlike the SDK's `auth`: the same table is
54
+ the target of `conversation.user_id`, and `f.tableRef` resolves through
55
+ `ObjectRef`, which has no numeric form.
56
+
57
+ ## Build a frontend on it — the whole path
58
+
59
+ Everything below is expanded later; this is the order to do it in.
60
+
61
+ **1. Generate the routes.** Never import a def into the browser for its
62
+ `getPath()` — the chat endpoints run an AI agent, so importing one drags the SDK
63
+ runtime (~236 kB min / ~62 kB gz) into the bundle. This file imports nothing:
64
+
65
+ ```bash
66
+ npx xanosdk routes ./xano/index.ts --emit xano/routes.gen.ts
67
+ ```
68
+
69
+ **2. Import the types — all type-only, so they erase.**
70
+
71
+ ```ts
72
+ import type {
73
+ PublicConversation, PublicMessage, // rows
74
+ SendMessageBody, ChatReply, // the one call that matters
75
+ ListConversationsResponse, ListMessagesResponse,
76
+ } from "@xano-sdk/chatbot";
77
+ import { routePath } from "../xano/routes.gen.js";
78
+ ```
79
+
80
+ **3. Four calls make a chat UI.** Every one takes `Authorization: Bearer <token>`
81
+ for your `authTable`.
82
+
83
+ | do | call |
84
+ |---|---|
85
+ | list threads | `routePath("GET chat/conversations")` → `ListConversationsResponse` |
86
+ | open a thread | `routePath("POST chat/conversations/create")` → `PublicConversation` |
87
+ | load a thread | `routePath("GET chat/conversations/{conversation_id}/messages", { conversation_id })` → `ListMessagesResponse` |
88
+ | send | `routePath("POST chat/conversations/{conversation_id}/send", { conversation_id })`, body `SendMessageBody` → `ChatReply` |
89
+
90
+ `SendMessageBody` is `{ content }` — the id rides in the path, not the body.
91
+
92
+ **4. Render.** Assistant turns as **Markdown with raw HTML disabled**; `role:
93
+ "user"` turns as **plain text**. The reply is model output shaped by user input.
94
+
95
+ **5. Four behaviours to build around**, each with its own section below:
96
+
97
+ - **Serialize sends per thread.** Keep one in flight and disable the composer
98
+ while pending — concurrent sends to one thread scramble it.
99
+ - **Trim before sending.** Whitespace-only content is refused with `400`.
100
+ - **Handle `401` (sign out), `404` (thread gone — `404` *is* the permission
101
+ failure), `429` (rate limited, back off).**
102
+ - **No pagination and no streaming.** Reads are capped and silently truncated;
103
+ a reply arrives whole, so show one pending state.
104
+
105
+ That is the complete contract. Adding `tools` later changes none of it — see
106
+ *What a client sees when a tool runs*.
107
+
108
+ ## Endpoints (default `routePrefix: "chat"`)
109
+
110
+ Authenticated family (`authenticated: true`, the default; `Authorization: Bearer`,
111
+ scoped to `auth("id")`):
112
+
113
+ - `POST chat/conversations/create` → the conversation
114
+ - `GET chat/conversations` → the caller's conversations, most recently active first
115
+ - `GET chat/conversations/{conversation_id}/messages` → transcript, oldest first
116
+ - `POST chat/conversations/{conversation_id}/send` `{ content }` →
117
+ `{ conversation_id, reply, message_id, tool_calls }`
118
+ - `DELETE chat/conversations/{conversation_id}` → `null`
119
+ - `POST chat/conversations/{conversation_id}/claim` `{ session_token }` → the
120
+ claimed conversation *(only when `guest: true`)*
121
+
122
+ Guest family (`guest: true`, off by default; public, scoped by `session_token`):
123
+
124
+ - `POST chat/guest/conversations/create` → the conversation **plus `session_token`**
125
+ - `GET chat/guest/conversations/{conversation_id}/messages`
126
+ - `POST chat/guest/conversations/{conversation_id}/send`
127
+ - `POST chat/guest/conversations/{conversation_id}/delete` *(POST, not DELETE, to
128
+ keep the token out of the URL)*
129
+
130
+ **Route names are not RESTful verb pairs.** A verb pair on one name is legal in the
131
+ SDK, but the names cannot change: a query's identity includes its name, so a rename
132
+ moves every endpoint's identity and every consumer's `xano.lock` with it.
133
+ `create` / `send` also read as Xano-idiomatic, the same shape as `auth/signup`.
134
+
135
+ ⚠ In YOUR endpoints, check which SDK version you are on before reaching for a verb pair.
136
+
137
+ ## Options
138
+
139
+ `authTable` (required unless `authenticated: false`) · `userIdType` (`"int"`) ·
140
+ `authenticated` (`true`) · `guest` (`false`) · `llm` (`{ type: "xano-free" }`) ·
141
+ `historyLimit` (`20`) · `listLimit` (`100`) · `transcriptLimit` (`200`) ·
142
+ `canonical` (unset) · `history` (`false`) · `routePrefix` (`"chat"`) · `names` ·
143
+ `tags` (`["xano:chatbot"]`) · `tools` (`[]`) · `rateLimit` (`{ max: 20, ttl: 60 }`,
144
+ `false` disables).
145
+
146
+ - **`llm.prompt` / `llm.messages` are refused** at compile time and at runtime.
147
+ This package owns the run prompt because that is how the transcript reaches the
148
+ model, and Xano stores ONE prompt behind a `prompt_type` discriminator — a
149
+ supplied one would replace the history, not add to it. Use `systemPrompt`.
150
+ - **Request history defaults `false`**, against the engine's inherit-on: the
151
+ request body carries the user's message text and, on the guest family, the
152
+ `session_token` granting access to the whole thread.
153
+ - **`canonical`** pins the API group's URL segment so `getPath()` resolves with no
154
+ lock file. Unset, identity comes from `xano.lock` and a bare `getPath()` throws.
155
+
156
+ ## Architecture & Behaviour
157
+
158
+ - **`messages` template:** Sends structured message objects natively to the model
159
+ engine rather than interpolating raw text into a prompt, optimizing token usage.
160
+ - **Message validation:** `content` is required (`min: 1`) and automatically trimmed
161
+ at input; `role` is typed as an enum (`["user", "assistant", "system"]`).
162
+ - **Binary endpoint authentication:** Authenticated endpoints require a valid token,
163
+ while guest endpoints operate publicly using capability tokens. Separate endpoint
164
+ families ensure clean security boundaries.
165
+
166
+ ## Factories, not module-level defs
167
+
168
+ There is no `import { conversationTable }`. `f.tableRef` resolves its target's
169
+ guid eagerly at column-construction time, so a module-level def would bake in one
170
+ auth table forever. Use `createChatbot`, which builds everything and registers
171
+ nothing:
172
+
173
+ ```ts
174
+ const bot = createChatbot({ authTable: userTable });
175
+ xano.registerTables([bot.conversation, bot.message])
176
+ .registerAgents([bot.agent])
177
+ .registerFunctions([bot.replyFn])
178
+ .registerApiGroups([bot.group])
179
+ .registerQueries([bot.authenticated.sendMessage]);
180
+ ```
181
+
182
+ Dependencies travel together (queries need both tables, the reply function and the
183
+ agent). `registerChatbot(xano, opts)` does all of it and returns the same
184
+ `Chatbot` set with the instance added as `.xano`. Calling it twice on one instance
185
+ throws; two chatbots in one workspace need distinct `routePrefix` **and** `names`.
186
+
187
+ ## Typed client (frontend)
188
+
189
+ Every request and response type is exported. Import them **type-only** — they
190
+ erase, so nothing reaches the bundle:
191
+
192
+ ```ts
193
+ import type { SendMessageParams, SendMessageBody, ChatReply } from "@xano-sdk/chatbot";
194
+
195
+ const params: SendMessageParams = { conversation_id: 42 }; // → interpolated into the URL
196
+ const body: SendMessageBody = { content: "Hello" }; // → the JSON body
197
+ ```
198
+
199
+ **Naming rule:** `<HandleName><Part>`, where the handle name is the property on
200
+ `bot.authenticated` / `bot.guest` — `sendMessage` → `SendMessage…`.
201
+
202
+ | part | is | goes |
203
+ |---|---|---|
204
+ | `…Params` | path parameters | interpolated into the URL |
205
+ | `…Query` | query-string parameters | `?a=b` |
206
+ | `…Body` | the JSON body | the request body |
207
+ | `…Input` | all of the above at once (what the SDK derives) | — |
208
+ | `…Response` | what comes back | — |
209
+
210
+ A part an endpoint does not take is **not exported**, so the existence of a name
211
+ answers "does this take a body?". Do not send `…Input` as the body — it contains
212
+ the path params, which the endpoint reads off the URL.
213
+
214
+ ### `ChatbotEndpoints` — the whole surface as one map
215
+
216
+ Keyed by the same names as the def handles, so a client can be written or
217
+ generated from it alone:
218
+
219
+ ```ts
220
+ type Send = ChatbotEndpoints["sendMessage"];
221
+ // verb "POST" · route "conversations/{conversation_id}/send" · auth "token"
222
+ // params { conversation_id } · query never · body { content } · response ChatReply
223
+ ```
224
+
225
+ Keys: `createConversation`, `listConversations`, `listMessages`, `sendMessage`,
226
+ `deleteConversation`, `claimConversation`, and `guestCreateConversation`,
227
+ `guestListMessages`, `guestSendMessage`, `guestDeleteConversation`. Each entry
228
+ carries `verb`, `route` (relative to `routePrefix`), `auth`
229
+ (`"token"` | `"session_token"` | `"none"`), `params`, `query`, `body`, `response`
230
+ — `never` where a part does not apply.
231
+
232
+ ⚠ `session_token` is a bearer capability, and the map records where it travels:
233
+ `guestListMessages` is a GET so it rides in the **query string** (and therefore
234
+ into access logs); `guestDeleteConversation` is a POST **specifically** so it
235
+ rides in the body instead.
236
+
237
+ ⚠ **Do not call `createChatbot()` in browser code to reach a def.** It is a
238
+ runtime call: the `s.*`/`c.*` factories in each stack execute at module load and
239
+ cannot be tree-shaken, so you pay ~236 kB min / ~62 kB gz for a type that erases.
240
+ Import the types above, or `import type` the `bot` handle your `xano/index.ts`
241
+ exports:
242
+
243
+ ```ts
244
+ import type { bot } from "../xano/index.js"; // erases completely
245
+ type Send = typeof bot.authenticated.sendMessage;
246
+ ```
247
+
248
+ For **paths**, do not import defs either — `xanosdk routes <entry> --emit
249
+ xano/routes.gen.ts` emits verbs and paths as plain data that imports nothing.
250
+
251
+ ## Concurrent sends to one thread — serialize client-side
252
+
253
+ A send operation writes the user turn, retrieves history, executes the AI
254
+ model, and records the assistant reply. Because model generation is asynchronous,
255
+ concurrent sends to the same conversation thread can interleave: multiple user
256
+ turns may be written before an assistant reply returns, causing subsequent model
257
+ calls to see in-flight messages.
258
+
259
+ **Recommended client practice:** Keep one send request in flight per conversation
260
+ and disable the composer while awaiting a reply.
261
+
262
+ Transcripts are ordered by `created_at` timestamp rather than auto-incrementing ID.
263
+
264
+ ## Rate limiting — ON by default
265
+
266
+ The guest family is public and every send invokes an LLM call, so `rateLimit`
267
+ defaults to `{ max: 20, ttl: 60 }` (20 requests per 60s per caller) on the
268
+ endpoints that consume resources: both `send` endpoints and
269
+ `guest/conversations/create`. Reads are not rate-limited.
270
+
271
+ ```ts
272
+ registerChatbot(xano, { authTable, rateLimit: { max: 5, ttl: 30 } });
273
+ registerChatbot(xano, { authTable, rateLimit: false }); // remove it entirely
274
+ ```
275
+
276
+ The rate limiter runs first, before database lookups, so unauthorized probes
277
+ consume only the caller's request budget. Rate limit buckets are namespaced by
278
+ `routePrefix`. Authenticated endpoints key on `auth("id")`, while guest endpoints
279
+ key on `sys.remoteIp()`.
280
+
281
+ ## Limits truncate silently — there is no pagination
282
+
283
+ No read endpoint paginates. Each caps its result and returns the capped list with
284
+ no cursor or total count:
285
+
286
+ | option | default | caps | drops |
287
+ |---|---|---|---|
288
+ | `transcriptLimit` | `200` | messages per transcript read | the **oldest** messages |
289
+ | `listLimit` | `100` | conversations per list read | the least recently active |
290
+ | `historyLimit` | `20` | **messages** replayed to the model per send, *including the current one* | the oldest messages |
291
+
292
+ Consequences to design around:
293
+ - Do not implement pagination controls against these endpoints.
294
+ - `historyLimit` counts messages including the turn being sent. For example, at
295
+ `historyLimit: 2` the model receives only the previous assistant message and
296
+ current user message.
297
+ - `historyLimit` is independent of `transcriptLimit`. Raise options in configuration
298
+ if needed.
299
+
300
+ ## Giving the agent tools
301
+
302
+ The chat agent is tool-less by default. Pass `tools` to allow the assistant to
303
+ call workspace functions and query external data on demand:
304
+
305
+ ```ts
306
+ import { tool, input, s, inp, ref } from "@xano/sdk";
307
+
308
+ const orderStatus = tool({
309
+ name: "order_status",
310
+ description: "Look up the delivery status of an order by its id.",
311
+ input: { order_id: input.int({ required: true }) },
312
+ stack: [s.db.get({ table: orders, fieldName: "id", fieldValue: inp("order_id"), as: "row" })],
313
+ response: ref("row"),
314
+ });
315
+
316
+ const bot = registerChatbot(xano, { authTable: userTable, tools: [orderStatus] });
317
+ bot.xano.registerTools([orderStatus]); // ← REQUIRED: register tools on the workspace
318
+ export default bot.xano;
319
+ ```
320
+
321
+ ⚠ **You must register the tool yourself.** This package only references tools.
322
+ Registering them on the workspace ensures proper resolution at deployment.
323
+
324
+ Key behaviors when adding tools:
325
+ - **`llm.maxSteps`:** Bounds reasoning/tool steps (defaults to `5`).
326
+ - **Tool authorization:** a tool has **no caller identity unless its toolset
327
+ entry names an auth table**. With `authTable` set, this package gives every
328
+ entry that names none `auth: <authTable>`, so `auth("id")` inside the tool
329
+ binds the chatting user. See *Per-tool auth* below before you write `auth()`.
330
+ - **Transcript context:** Tool outputs returned to the model become part of the
331
+ conversation context. Ensure tools return concise, relevant data.
332
+
333
+ ### Per-tool auth — read this before writing `auth()` in a tool
334
+
335
+ A toolset entry carries its own `auth`, and the engine's default is
336
+ `auth: false` — a **public** tool stack. `auth("id")` in a public stack does not
337
+ resolve to null; it raises `ERROR_CODE_ACCESS_DENIED` on the first statement
338
+ that reads it, the agent swallows the throw, and the model still answers "I
339
+ saved that note". Nothing is written and nothing fails loudly.
340
+
341
+ So when `authTable` is set, **this package scopes your tools for you**:
342
+
343
+ ```ts
344
+ tools: [saveNote] // → { tool: saveNote, auth: userTable }
345
+ tools: [{ tool: saveNote }] // → { tool: saveNote, auth: userTable }
346
+ tools: [{ tool: saveNote, auth: other }] // → left alone
347
+ tools: [{ tool: ping, auth: false }] // → left PUBLIC — the explicit opt-out
348
+ ```
349
+
350
+ Rules that follow from it:
351
+
352
+ - **`{ tool, auth: false }` is now the only spelling that produces a public
353
+ tool.** A tool written that way must not call `auth()`; scope what it reads by
354
+ the arguments the model supplies.
355
+ - **A guest-only bot** (`{ authenticated: false, guest: true }`) has no auth
356
+ table, so nothing is scoped and every tool is public. `auth()` cannot work
357
+ there at all.
358
+ - **Both families at once is a hazard the package cannot resolve.** One agent
359
+ serves both, and an entry carries one `auth`: a scoped tool has no identity to
360
+ bind on a public guest send. `createChatbot` warns, and the fix is either
361
+ `{ tool, auth: false }` plus no `auth()` in the tool, or a second
362
+ `registerChatbot` install (own `routePrefix` and `names`) for the guest bot.
363
+
364
+ ### What a client sees when a tool runs
365
+
366
+ **The API contract remains identical.** Whether tools are enabled or not:
367
+
368
+ | | with tools | without tools |
369
+ |---|---|---|
370
+ | `send` response | `ChatReply` — `{ conversation_id, reply, message_id, tool_calls }` | identical |
371
+ | `reply` | a plain string, still Markdown | identical |
372
+ | `tool_calls` | the **names** of the tools that ran, in call order | always `[]` |
373
+ | transcript rows added per send | **2** — the user turn and the final answer | 2 |
374
+ | tool calls in the transcript | **none** | — |
375
+
376
+ A client interacting with the chatbot uses the exact same interface: calling
377
+ `send` returns the final answer in `reply`, and the stored transcript contains
378
+ clean user and assistant turns.
379
+
380
+ `tool_calls` is always an **array of strings**, never null, so
381
+ `reply.tool_calls.length` needs no branch on how the bot was configured. Names
382
+ only, and deliberately: a call record also carries the arguments the model
383
+ produced and whatever the tool returned, which is data no client asked for.
384
+
385
+ ⚠ It is **not an audit log.** It reports which tools the model reached for on
386
+ this run, not which of them succeeded. To know a tool worked, read the rows it
387
+ should have written.
388
+
389
+ Key considerations when enabling tools:
390
+ - **Execution loop:** The agent loops (calling tools and evaluating responses)
391
+ until generating a final reply, bounded by `llm.maxSteps` (default `5`). All
392
+ tool calls complete within the single `send` request.
393
+ - **Latency:** Tool execution adds to request duration. Keep tool functions
394
+ fast and keep `maxSteps` bounded to your use case.
395
+ - **Client visibility:** `tool_calls` names what ran; the arguments, results and
396
+ intermediate turns stay server-side and are not written to
397
+ `conversation_message`.
398
+ - **Security:** Model output remains untrusted content. Keep raw HTML disabled
399
+ in frontend renderers.
400
+
401
+ ### The model has to be told the tools exist
402
+
403
+ When `tools` are configured, this package automatically appends instructions to
404
+ the system prompt:
405
+
406
+ > You have tools available. When a question needs information you do not have,
407
+ > call the appropriate tool rather than guessing or saying you do not know. Use
408
+ > what a tool returns to answer in your own words — never paste the raw tool
409
+ > response or its wrapper into your reply.
410
+
411
+ This ensures the model proactively executes available tools when needed and
412
+ translates raw tool response structures into natural language replies. Exported
413
+ as `TOOLS_SYSTEM_PROMPT` for reference or reuse.
414
+
415
+ ### Structured output is deliberately not configurable
416
+
417
+ `AgentDef` supports an `output` schema; this package pins none and exposes no
418
+ option for it. `ChatReply.reply` is declared `string` and the send endpoints hand
419
+ `run.result` straight back, so a structured-output schema would make `.result` an
420
+ object where every consumer's type — and every markdown renderer — expects text.
421
+
422
+ If you want structured data out of a conversation, give the agent a **tool** that
423
+ records it and keep the reply itself free text. That also keeps the thing the
424
+ user reads and the thing your system stores from competing for one field.
425
+
426
+ ## Errors — what a client must handle
427
+
428
+ Common HTTP status codes returned by the chatbot endpoints (wrapped in Xano's
429
+ standard `{ code, message, payload? }` envelope):
430
+
431
+ | status | `code` | when | client should |
432
+ |---|---|---|---|
433
+ | `400` | `ERROR_CODE_INPUT_ERROR` | a required param is absent or empty | fix the request; not retryable |
434
+ | `401` | `ERROR_CODE_UNAUTHORIZED` | no token, or an invalid/expired one (tokens last 24h) | sign the user out and re-authenticate |
435
+ | `403` | `ERROR_CODE_ACCESS_DENIED` | a guest token on a claimed thread; duplicate claim attempt | stop using the session token; prompt to sign in |
436
+ | `404` | `ERROR_CODE_NOT_FOUND` | thread missing, not owned by user, or invalid guest token | treat the thread as unavailable; refresh list |
437
+ | `429` | `ERROR_CODE_TOO_MANY_REQUESTS` | rate limit exceeded | back off and retry after the window |
438
+ | `500` | — | agent execution failure or empty reply | retryable; user turn may already be recorded |
439
+
440
+ Key behavior notes:
441
+ - **Unified 404s for security:** A missing thread and another user's thread both
442
+ return `404 Not Found` to prevent ID enumeration.
443
+ - **Empty parameter handling:** Empty strings (`""`) are treated as missing
444
+ parameters and return `400 Bad Request`.
445
+ - **Automatic trimming:** User message `content` is automatically trimmed before
446
+ storage and processing. Whitespace-only messages are rejected with `400`.
447
+
448
+ ## Types
449
+
450
+ `Conversation`, `PublicConversation`, `Message`, `PublicMessage`, `MessageRole`,
451
+ `ChatReply`, plus `ChatbotOptions` / `ChatbotAuthTable` / `ChatbotLlmOptions`. All
452
+ erase at compile time, so a frontend can `import type` them without pulling a def
453
+ into the bundle.
454
+
455
+ **Render `reply` as Markdown.** Any frontend works, but replies read far better
456
+ rendered than as plain text, so the default system prompt asks the model for light
457
+ markdown (emphasis, lists, fenced code). Use a markdown component
458
+ (`<Markdown>{message.content}</Markdown>`) rather than a text node, or the reader
459
+ sees literal `**asterisks**`. Three caveats:
460
+
461
+ - The reply is model output shaped by user input, so treat it as **untrusted**:
462
+ keep raw HTML disabled (the default in `react-markdown`/`marked`) or sanitize
463
+ before `dangerouslySetInnerHTML`. The prompt telling the model not to emit HTML
464
+ is a nudge, not a control.
465
+ - Render `role: "user"` turns as **plain text** — those are verbatim user input.
466
+ - Building a plain-text UI? Override `llm.systemPrompt` to drop the formatting
467
+ instruction, rather than stripping markup client-side.
468
+
469
+ `ChatReply` is the only **declared** response shape — `reply` is the agent run's
470
+ `.result`, which the SDK resolves to `unknown` because the agent carries no
471
+ structured-output schema. Every other response is derived from its `output` list.
472
+
473
+ ## Read before production
474
+
475
+ - A guest `session_token` is a **bearer capability**, not an identity: whoever
476
+ holds it can read and continue that thread. No expiry; claiming a thread is the
477
+ only thing that ends its authority.
478
+ - **Reads are not rate-limited**; `send` and guest create are (`rateLimit`, on by
479
+ default). The cap is per caller, not a spend budget — it does not bound what
480
+ your provider bill can reach.
481
+ - **Nothing here moderates** input or output.
482
+ - Conversations grow without limit; there is no pruning or retention. You own the
483
+ lifecycle of both tables.
484
+ - A missing thread and someone else's thread both return `notfound`, so ids cannot
485
+ be enumerated.
486
+
487
+ Full detail: [README.md](README.md).
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@xano-sdk/chatbot",
3
+ "version": "1.0.0",
4
+ "description": "Plug-and-play Xano AI chatbot (conversation/message tables + a Xano AI agent + chat endpoints) as typed Xano SDK defs.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "author": "Xano",
11
+ "keywords": [
12
+ "xano",
13
+ "xanosdk",
14
+ "xanoscript",
15
+ "chatbot",
16
+ "chat",
17
+ "ai",
18
+ "agent",
19
+ "llm",
20
+ "conversation",
21
+ "backend",
22
+ "codegen"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/xanots/chatbot.git"
27
+ },
28
+ "homepage": "https://github.com/xanots/chatbot#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/xanots/chatbot/issues"
31
+ },
32
+ "sideEffects": false,
33
+ "xanosdk": {
34
+ "register": "registerChatbot",
35
+ "returns": "handle",
36
+ "options": {
37
+ "authTable": {
38
+ "package": "@xano-sdk/auth",
39
+ "export": "userTable"
40
+ }
41
+ }
42
+ },
43
+ "main": "./dist/index.js",
44
+ "types": "./dist/index.d.ts",
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/index.d.ts",
48
+ "import": "./dist/index.js"
49
+ },
50
+ "./package.json": "./package.json"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "!dist/**/*.map",
55
+ "README.md",
56
+ "AGENTS.md",
57
+ "llms.txt"
58
+ ],
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "tsc --noEmit && vitest run",
63
+ "test:watch": "vitest",
64
+ "lint": "eslint .",
65
+ "fixture:regen": "tsx scripts/regen-golden.ts",
66
+ "prepublishOnly": "npm run build",
67
+ "release": "npm publish --access public",
68
+ "release:beta": "npm version prerelease --preid=beta -m \"chore(release): %s\" && npm publish --tag beta --access public"
69
+ },
70
+ "engines": {
71
+ "node": ">=20"
72
+ },
73
+ "peerDependencies": {
74
+ "@xano/sdk": ">=1.0.0 <2.0.0"
75
+ },
76
+ "devDependencies": {
77
+ "@eslint/js": "^9.0.0",
78
+ "@types/node": "^20.0.0",
79
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
80
+ "@typescript-eslint/parser": "^8.0.0",
81
+ "@xano/sdk": "1.0.0",
82
+ "eslint": "^9.0.0",
83
+ "tsup": "^8.0.0",
84
+ "tsx": "^4.23.1",
85
+ "typescript": "^5.5.0",
86
+ "vitest": "^2.0.0"
87
+ }
88
+ }