UNPKG

chat

Version:

Unified chat abstraction for Slack, Teams, Google Chat, and Discord

138 lines (100 loc) 5.2 kB
--- title: Conversation History description: Persist messages per user across every platform — for LLM context, audit, or compliance. type: guide prerequisites: - /docs/state related: - /docs/handling-events - /docs/api/transcripts --- Bots that hold context across a user's conversations need somewhere to store it. The platform's own message history won't do — a user might talk to your bot in Slack today and Discord tomorrow, and you want the same memory to follow them. `bot.transcripts` keeps a per-user transcript in your state adapter, keyed by a stable identifier you choose (an email, an internal user ID, anything that's the same person no matter where they are). ## Setup You opt in by setting two fields on `ChatConfig`: ```typescript title="lib/bot.ts" lineNumbers import { Chat } from "chat"; import { createSlackAdapter } from "@chat-adapter/slack"; import { createDiscordAdapter } from "@chat-adapter/discord"; import { createRedisState } from "@chat-adapter/state-redis"; const bot = new Chat({ userName: "mybot", adapters: { slack: createSlackAdapter(), discord: createDiscordAdapter(), }, state: createRedisState({ url: process.env.REDIS_URL! }), // Resolve the cross-platform identifier for an inbound message. // Return null for messages you don't want to remember. identity: ({ author }) => author.email ?? null, // Storage tuning. retention is the list TTL, refreshed on every append. transcripts: { retention: "30d", maxPerUser: 200, }, }); ``` `transcripts` and `identity` are paired — set one without the other and the constructor throws. This keeps the API loud rather than silently no-op'ing on every call. ## Building LLM context The most common pattern: append the user's message, build a prompt from recent transcript entries, post the reply, append the reply too. ```typescript title="lib/bot.ts" lineNumbers bot.onSubscribedMessage(async (thread, msg) => { await bot.transcripts.append(thread, msg); const recent = await bot.transcripts.list({ userKey: msg.userKey!, limit: 20, }); const reply = await generateReply(recent, msg); await thread.post(reply); await bot.transcripts.append( thread, { role: "assistant", text: reply }, { userKey: msg.userKey! } ); }); ``` A few things worth knowing: - **`msg.userKey`** is set automatically from your `identity` resolver before your handler runs. If the resolver returned `null`, it stays `undefined` and the `append` call no-ops. - **Bot replies are explicit.** The SDK doesn't auto-capture `thread.post()` output — you decide what gets remembered. That's important for retries, intermediate streaming chunks, and anything you don't want feeding back into the model later. - **Order is chronological.** `list` returns oldest-first, ready to feed into a model. Set `limit` to keep prompts bounded. ## Identity resolution `identity` runs once per inbound message during dispatch. The `author`, `message`, and `adapter` name are all available: ```typescript identity: async ({ adapter, author, message }) => { // Look up by email when the platform exposes it if (author.email) { return author.email; } // Or map a platform user to an internal ID return await lookupUser(adapter, author.userId); } ``` Return `null` when you can't resolve a key. The SDK won't fall back to a platform-specific ID — that would silently fragment a user's transcript across platforms, which is exactly what this feature is here to prevent. If your resolver throws, the SDK logs a warning and dispatches the message without a `userKey`. Handlers still run; only the persistence is skipped. ## Filtering entries `list` accepts a few filters. They compose, and they're applied after `getList` — useful for narrowing prompts without restructuring storage. ```typescript // Recent N across all platforms await bot.transcripts.list({ userKey: "mike@acme.com", limit: 50 }); // Single platform await bot.transcripts.list({ userKey: "mike@acme.com", platforms: ["slack"] }); // Single thread await bot.transcripts.list({ userKey: "mike@acme.com", threadId: "slack:C123:1234.5678", }); // Only the user's own messages await bot.transcripts.list({ userKey: "mike@acme.com", roles: ["user"] }); ``` ## Deleting a user's transcript For data-subject requests or simple "forget me" flows: ```typescript await bot.transcripts.delete({ userKey: "mike@acme.com" }); // → { deleted: 47 } ``` This wipes every entry stored under the key. Single-entry and time-range deletes aren't part of the API — `appendToList` doesn't support them safely under concurrent writes. ## Where it's stored `bot.transcripts` is backed by `StateAdapter.appendToList` / `getList` / `delete`. Every built-in state adapter (`memory`, `redis`, `ioredis`, `pg`) supports these primitives, so this works on whichever one you've already configured. Entries are written under the key `transcripts:user:{userKey}` as a capped list. `appendToList` is atomic, so concurrent inbound messages on the same user don't race. ## Reference See [Transcripts](/docs/api/transcripts) for full type signatures, configuration options, and the entry shape.