mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-13 09:46:14 +00:00
plans: Update 20260207-support-bot-implementation.md
This commit is contained in:
@@ -1,301 +1,362 @@
|
||||
# SimpleX Support Bot — Implementation Plan
|
||||
|
||||
## Context
|
||||
## 1. Executive Summary
|
||||
|
||||
SimpleX Chat needs a support bot that handles customer inquiries via business chat, optionally routes to Grok AI, and escalates to human team members. The bot implements the product spec in `apps/multiplatform/plans/20260207-support-bot.md`. Codebase at `apps/simplex-chat-support-bot/`.
|
||||
SimpleX Chat support bot — standalone Node.js app using `simplex-chat-nodejs` native NAPI binding. Two `ChatApi` instances (main bot + Grok agent identity) in one process, each with own SQLite database. No external CLI processes. Implements 4-step flow: Welcome → TeamQueue → GrokMode/TeamPending → TeamLocked.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture](#1-architecture)
|
||||
2. [Project Structure](#2-project-structure)
|
||||
3. [Configuration](#3-configuration)
|
||||
4. [State Machine](#4-state-machine)
|
||||
5. [Two-ChatClient Coordination](#5-two-chatclient-coordination)
|
||||
6. [Bot Initialization](#6-bot-initialization)
|
||||
7. [Event Processing](#7-event-processing)
|
||||
8. [Message Routing](#8-message-routing)
|
||||
9. [Team Forwarding](#9-team-forwarding)
|
||||
10. [Grok API Integration](#10-grok-api-integration)
|
||||
11. [One-Way Gate Logic](#11-one-way-gate-logic)
|
||||
12. [Bot Commands Setup](#12-bot-commands-setup)
|
||||
13. [Message Templates](#13-message-templates)
|
||||
14. [Error Handling](#14-error-handling)
|
||||
15. [Implementation Sequence](#15-implementation-sequence)
|
||||
16. [Verification](#16-verification)
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Support Bot Process (TS) │
|
||||
│ │
|
||||
│ mainClient ──WS──> simplex-chat CLI (5225) │ Business address, event loop
|
||||
│ grokClient ──WS──> simplex-chat CLI (5226) │ Grok identity, auto-join groups
|
||||
│ │
|
||||
│ conversations: Map<groupId, State> │
|
||||
│ grokGroupMap: Map<mainGroupId, grokGroupId> │
|
||||
│ GrokApiClient → api.x.ai/v1/chat/completions│
|
||||
└──────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Support Bot Process (Node.js) │
|
||||
│ │
|
||||
│ mainChat: ChatApi ← ChatApi.init("./data/bot") │
|
||||
│ • Business address, event routing, state mgmt │
|
||||
│ • DB: data/bot_chat.db + data/bot_agent.db │
|
||||
│ │
|
||||
│ grokChat: ChatApi ← ChatApi.init("./data/grok") │
|
||||
│ • Grok identity, auto-joins groups │
|
||||
│ • DB: data/grok_chat.db + data/grok_agent.db │
|
||||
│ │
|
||||
│ conversations: Map<groupId, ConversationState> │
|
||||
│ grokGroupMap: Map<mainGroupId, grokGroupId> │
|
||||
│ GrokApiClient → api.x.ai/v1/chat/completions │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Two `simplex-chat` CLI servers must run externally:
|
||||
- CLI 1 (port 5225): Main bot profile with business address
|
||||
- CLI 2 (port 5226): Grok agent profile (pre-configured as contact of main bot)
|
||||
- Single Node.js process, no external dependencies except Grok API
|
||||
- Two `ChatApi` instances via native NAPI — each embeds simplex-chat core
|
||||
- Business address auto-accept creates a group per customer (business chat = special group)
|
||||
- Grok agent is a separate identity that gets invited as group member, making Grok appear as a separate participant per spec
|
||||
- Cross-instance group ID correlation via protocol-level `memberId` (string, same across both databases)
|
||||
|
||||
The bot connects to both via WebSocket using the `simplex-chat` npm package.
|
||||
|
||||
## 2. Project Structure
|
||||
## 3. Project Structure
|
||||
|
||||
```
|
||||
apps/simplex-chat-support-bot/
|
||||
├── package.json # deps: simplex-chat, @simplex-chat/types
|
||||
├── tsconfig.json # ES2022, Node module resolution
|
||||
├── tsconfig.json # ES2022, strict, Node16 module resolution
|
||||
├── src/
|
||||
│ ├── index.ts # Entry: parse args, create clients, run
|
||||
│ ├── config.ts # CLI arg parsing, env vars, Config type
|
||||
│ ├── bot.ts # SupportBot class: event loop, dispatch
|
||||
│ ├── state.ts # ConversationState union, transitions
|
||||
│ ├── grok.ts # Grok xAI API client, system prompt, history
|
||||
│ ├── messages.ts # All user-facing message templates
|
||||
│ └── util.ts # extractText, isWeekend, logging
|
||||
├── docs/
|
||||
│ └── simplex-context.md # Curated SimpleX docs injected into Grok prompt
|
||||
│ ├── index.ts # Entry: parse config, init instances, run
|
||||
│ ├── config.ts # CLI arg parsing, ID:name validation, Config type
|
||||
│ ├── bot.ts # SupportBot class: state mgmt, event dispatch, routing
|
||||
│ ├── state.ts # ConversationState union type
|
||||
│ ├── grok.ts # GrokApiClient: xAI API wrapper, system prompt, history
|
||||
│ ├── messages.ts # All user-facing message templates (verbatim from spec)
|
||||
│ └── util.ts # isWeekend, logging helpers
|
||||
├── data/ # SQLite databases (created at runtime)
|
||||
└── docs/
|
||||
└── simplex-context.md # Curated SimpleX docs injected into Grok system prompt
|
||||
```
|
||||
|
||||
## 3. Configuration
|
||||
## 4. Configuration — ID:name Format
|
||||
|
||||
All entity references use `ID:name` format. The bot validates each pair at startup against live data from `apiListContacts()` / `apiListGroups()`.
|
||||
|
||||
**CLI args:**
|
||||
|
||||
| Arg | Required | Default | Purpose |
|
||||
|-----|----------|---------|---------|
|
||||
| `--main-port` | No | 5225 | Main bot CLI WebSocket port |
|
||||
| `--grok-port` | No | 5226 | Grok agent CLI WebSocket port |
|
||||
| `--team-group` | Yes | — | GroupId for team message forwarding |
|
||||
| `--team-members` | Yes | — | Comma-separated contactIds of team members |
|
||||
| `--grok-contact-id` | Yes | — | ContactId of Grok agent in main bot's contacts |
|
||||
| `--group-links` | No | "" | Public group link(s) for welcome message |
|
||||
| `--timezone` | No | "UTC" | IANA timezone for weekend detection |
|
||||
| Arg | Required | Default | Format | Purpose |
|
||||
|-----|----------|---------|--------|---------|
|
||||
| `--db-prefix` | No | `./data/bot` | path | Main bot database file prefix |
|
||||
| `--grok-db-prefix` | No | `./data/grok` | path | Grok agent database file prefix |
|
||||
| `--team-group` | Yes | — | `ID:name` | Group for forwarding customer messages to team |
|
||||
| `--team-members` | Yes | — | `ID:name,...` | Comma-separated team member contacts |
|
||||
| `--grok-contact` | Yes* | — | `ID:name` | Grok agent's contactId in main bot's database |
|
||||
| `--group-links` | No | `""` | string | Public group link(s) for welcome message |
|
||||
| `--timezone` | No | `"UTC"` | IANA tz | For weekend detection (24h vs 48h) |
|
||||
| `--first-run` | No | false | flag | Auto-establish contact between bot and Grok agent |
|
||||
|
||||
*`--grok-contact` required unless `--first-run` is used.
|
||||
|
||||
**Env vars:** `GROK_API_KEY` (required) — xAI API key.
|
||||
|
||||
```typescript
|
||||
interface Config {
|
||||
mainPort: number
|
||||
grokPort: number
|
||||
teamGroupId: number
|
||||
teamMemberContactIds: number[]
|
||||
grokContactId: number
|
||||
dbPrefix: string
|
||||
grokDbPrefix: string
|
||||
teamGroup: {id: number; name: string}
|
||||
teamMembers: {id: number; name: string}[]
|
||||
grokContact: {id: number; name: string} | null // null during first-run
|
||||
groupLinks: string
|
||||
timezone: string
|
||||
grokApiKey: string
|
||||
firstRun: boolean
|
||||
}
|
||||
```
|
||||
|
||||
Parse via `process.argv` iteration (no deps needed). Fail fast on missing required args.
|
||||
**ID:name parsing:**
|
||||
```typescript
|
||||
function parseIdName(s: string): {id: number; name: string} {
|
||||
const i = s.indexOf(":")
|
||||
if (i < 1) throw new Error(`Invalid ID:name format: "${s}"`)
|
||||
return {id: parseInt(s.slice(0, i), 10), name: s.slice(i + 1)}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. State Machine
|
||||
**Startup validation** (exact API calls):
|
||||
|
||||
Keyed by `groupId` of business chat group. In-memory only (acceptable for MVP — restart resets conversations; team group retains forwarded messages).
|
||||
| What | API Call | Validation |
|
||||
|------|----------|------------|
|
||||
| Team group | `mainChat.apiListGroups(userId)` → find by `groupId === config.teamGroup.id` | Assert `groupProfile.displayName === config.teamGroup.name` |
|
||||
| Team members | `mainChat.apiListContacts(userId)` → find each by `contactId` | Assert `profile.displayName === member.name` for each |
|
||||
| Grok contact | `mainChat.apiListContacts(userId)` → find by `contactId === config.grokContact.id` | Assert `profile.displayName === config.grokContact.name` |
|
||||
|
||||
Fail-fast with descriptive error on any mismatch.
|
||||
|
||||
## 5. State Machine
|
||||
|
||||
Keyed by `groupId` of business chat group. In-memory (restart resets; team group retains forwarded messages).
|
||||
|
||||
```typescript
|
||||
type ConversationState =
|
||||
| { type: "welcome" }
|
||||
| { type: "teamQueue"; userMessages: string[] } // tracks msgs for Grok context
|
||||
| { type: "grokMode"; grokMemberGId: number; history: GrokMessage[] }
|
||||
| { type: "teamPending"; teamMemberGId: number; grokMemberGId?: number; history?: GrokMessage[] }
|
||||
| { type: "teamLocked"; teamMemberGId: number }
|
||||
| {type: "welcome"}
|
||||
| {type: "teamQueue"; userMessages: string[]}
|
||||
| {type: "grokMode"; grokMemberGId: number; history: GrokMessage[]}
|
||||
| {type: "teamPending"; teamMemberGId: number; grokMemberGId?: number; history?: GrokMessage[]}
|
||||
| {type: "teamLocked"; teamMemberGId: number}
|
||||
```
|
||||
|
||||
**Key**: `teamQueue.userMessages` accumulates user messages so they can be forwarded to Grok as initial context on `/grok` activation (spec: "Your message(s) have been forwarded").
|
||||
`teamQueue.userMessages` accumulates user messages for Grok initial context on `/grok` activation.
|
||||
|
||||
Transitions:
|
||||
**Transitions:**
|
||||
```
|
||||
welcome ──(1st msg)──> teamQueue (msg stored in userMessages)
|
||||
teamQueue ──(more msgs)──> teamQueue (appended to userMessages)
|
||||
welcome ──(1st user msg)──> teamQueue
|
||||
teamQueue ──(user msg)──> teamQueue (append to userMessages)
|
||||
teamQueue ──(/grok)──> grokMode (userMessages → initial Grok history)
|
||||
teamQueue ──(/team)──> teamPending
|
||||
grokMode ──(/team)──> teamPending (carries grokMemberGId + history)
|
||||
grokMode ──(msg)──> grokMode (history appended)
|
||||
teamPending ──(team member msg)──> teamLocked (grok removed)
|
||||
teamPending ──(/grok, grok present)──> still teamPending (grok answers)
|
||||
grokMode ──(user msg)──> grokMode (forward to Grok API, append to history)
|
||||
grokMode ──(/team)──> teamPending (carry grokMemberGId + history)
|
||||
teamPending ──(team member msg)──> teamLocked (remove Grok if present)
|
||||
teamPending ──(/grok, grok present)──> teamPending (forward to Grok, still usable)
|
||||
teamPending ──(/grok, no grok)──> reply "team mode"
|
||||
teamLocked ──(/grok)──> reply "team mode", stay locked
|
||||
teamLocked ──(any)──> no action (team sees directly)
|
||||
```
|
||||
|
||||
## 5. Two-ChatClient Coordination
|
||||
## 6. Two-Instance Coordination
|
||||
|
||||
**Problem**: When main bot invites Grok agent to a business chat group, the Grok agent's local `groupId` differs from the main bot's `groupId` (different databases).
|
||||
**Problem:** When main bot invites Grok agent to a business group, Grok agent's local `groupId` differs (different databases).
|
||||
|
||||
**Solution**: Shared in-process maps correlated via `memberId` (protocol-level, same across both databases).
|
||||
**Solution:** In-process maps correlated via protocol-level `memberId` (string, same across databases).
|
||||
|
||||
```typescript
|
||||
const pendingGrokJoins = new Map<string, number>() // memberId → mainGroupId
|
||||
const grokGroupMap = new Map<number, number>() // mainGroupId → grokGroupId
|
||||
const grokGroupMap = new Map<number, number>() // mainGroupId → grokLocalGroupId
|
||||
const reverseGrokMap = new Map<number, number>() // grokLocalGroupId → mainGroupId
|
||||
```
|
||||
|
||||
Flow:
|
||||
1. Main bot: `apiAddMember(mainGroupId, grokContactId, "member")` → response has `member.memberId`
|
||||
**Flow:**
|
||||
1. Main bot: `mainChat.apiAddMember(mainGroupId, grokContactId, "member")` → response `member.memberId`
|
||||
2. Store: `pendingGrokJoins.set(member.memberId, mainGroupId)`
|
||||
3. Grok agent event loop: `receivedGroupInvitation` → `evt.groupInfo.membership.memberId` matches → `apiJoinGroup(evt.groupInfo.groupId)` → store mapping
|
||||
4. Send Grok response: `grokClient.apiSendTextMessage(ChatType.Group, grokGroupMap.get(mainGroupId), text)`
|
||||
3. Grok agent receives `receivedGroupInvitation` event → `evt.groupInfo.membership.memberId` matches → `grokChat.apiJoinGroup(evt.groupInfo.groupId)` → store bidirectional mapping
|
||||
4. Send Grok response: `grokChat.apiSendTextMessage([T.ChatType.Group, grokGroupMap.get(mainGroupId)!], text)`
|
||||
|
||||
**Grok agent event loop** (minimal, in same process):
|
||||
**Grok agent event subscriptions:**
|
||||
```typescript
|
||||
async function runGrokAgentLoop(grokClient: ChatClient): Promise<void> {
|
||||
for await (const r of grokClient.msgQ) {
|
||||
const evt = r instanceof Promise ? await r : r
|
||||
if (evt.type === "receivedGroupInvitation") {
|
||||
const memberId = evt.groupInfo.membership.memberId
|
||||
const mainGroupId = pendingGrokJoins.get(memberId)
|
||||
if (mainGroupId !== undefined) {
|
||||
pendingGrokJoins.delete(memberId)
|
||||
const grokLocalGroupId = evt.groupInfo.groupId
|
||||
grokGroupMap.set(mainGroupId, grokLocalGroupId)
|
||||
await grokClient.apiJoinGroup(grokLocalGroupId)
|
||||
}
|
||||
}
|
||||
// Ignore all other events
|
||||
grokChat.on("receivedGroupInvitation", async ({groupInfo}) => {
|
||||
const memberId = groupInfo.membership.memberId
|
||||
const mainGroupId = pendingGrokJoins.get(memberId)
|
||||
if (mainGroupId !== undefined) {
|
||||
pendingGrokJoins.delete(memberId)
|
||||
grokGroupMap.set(mainGroupId, groupInfo.groupId)
|
||||
reverseGrokMap.set(groupInfo.groupId, mainGroupId)
|
||||
await grokChat.apiJoinGroup(groupInfo.groupId)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 7. Bot Initialization
|
||||
|
||||
**Main bot** uses `bot.run()` for setup automation (address, profile, commands), with only `events` parameter for full routing control:
|
||||
|
||||
```typescript
|
||||
let supportBot: SupportBot // set after bot.run returns
|
||||
|
||||
const [mainChat, mainUser, mainAddress] = await bot.run({
|
||||
profile: {displayName: "SimpleX Support", fullName: ""},
|
||||
dbOpts: {dbFilePrefix: config.dbPrefix},
|
||||
options: {
|
||||
addressSettings: {
|
||||
businessAddress: true,
|
||||
autoAccept: true,
|
||||
welcomeMessage: welcomeMessage(config.groupLinks),
|
||||
},
|
||||
commands: [
|
||||
{type: "command", keyword: "grok", label: "Ask Grok AI"},
|
||||
{type: "command", keyword: "team", label: "Switch to team"},
|
||||
],
|
||||
useBotProfile: true,
|
||||
},
|
||||
events: {
|
||||
acceptingBusinessRequest: (evt) => supportBot?.onBusinessRequest(evt),
|
||||
newChatItems: (evt) => supportBot?.onNewChatItems(evt),
|
||||
leftMember: (evt) => supportBot?.onLeftMember(evt),
|
||||
deletedMemberUser: (evt) => supportBot?.onDeletedMemberUser(evt),
|
||||
groupDeleted: (evt) => supportBot?.onGroupDeleted(evt),
|
||||
connectedToGroupMember: (evt) => supportBot?.onMemberConnected(evt),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Grok agent** uses direct ChatApi:
|
||||
```typescript
|
||||
const grokChat = await ChatApi.init(config.grokDbPrefix)
|
||||
let grokUser = await grokChat.apiGetActiveUser()
|
||||
if (!grokUser) grokUser = await grokChat.apiCreateActiveUser({displayName: "Grok AI", fullName: ""})
|
||||
await grokChat.startChat()
|
||||
// Subscribe Grok event handlers (receivedGroupInvitation)
|
||||
```
|
||||
|
||||
**First-run mode** (`--first-run`):
|
||||
1. Both instances init and create users
|
||||
2. Main bot: `mainChat.apiCreateLink(mainUser.userId)` → invitation link
|
||||
3. Grok agent: `grokChat.apiConnectActiveUser(invLink)`
|
||||
4. Main bot: `mainChat.wait("contactConnected", 60000)` — wait for connection
|
||||
5. Print: "Grok contact established. ContactId=X. Use: --grok-contact X:GrokAI"
|
||||
6. Exit (user restarts without `--first-run`)
|
||||
|
||||
**Startup validation** (after init, before event loop):
|
||||
1. `mainChat.apiListContacts(mainUser.userId)` → validate `--team-members` and `--grok-contact` ID:name pairs
|
||||
2. `mainChat.apiListGroups(mainUser.userId)` → validate `--team-group` ID:name pair
|
||||
|
||||
## 8. Event Processing
|
||||
|
||||
**Main bot event handlers:**
|
||||
|
||||
| Event | Handler | Action |
|
||||
|-------|---------|--------|
|
||||
| `acceptingBusinessRequest` | `onBusinessRequest` | `conversations.set(groupInfo.groupId, {type: "welcome"})` |
|
||||
| `newChatItems` | `onNewChatItems` | For each chatItem: identify sender, extract text, dispatch to routing |
|
||||
| `leftMember` | `onLeftMember` | If customer left → delete state. If team member left → revert to teamQueue. If Grok left → clear grokMemberGId. |
|
||||
| `deletedMemberUser` | `onDeletedMemberUser` | Bot removed from group → delete state |
|
||||
| `groupDeleted` | `onGroupDeleted` | Delete state, delete grokGroupMap entry |
|
||||
| `connectedToGroupMember` | `onMemberConnected` | Log for debugging |
|
||||
|
||||
**Sender identification in `newChatItems`:**
|
||||
```typescript
|
||||
for (const ci of evt.chatItems) {
|
||||
const {chatInfo, chatItem} = ci
|
||||
if (chatInfo.type !== "group") continue
|
||||
const groupInfo = chatInfo.groupInfo
|
||||
if (!groupInfo.businessChat) continue // only process business chats
|
||||
const groupId = groupInfo.groupId
|
||||
const state = conversations.get(groupId)
|
||||
if (!state) continue
|
||||
|
||||
if (chatItem.chatDir.type === "groupSnd") continue // our own message
|
||||
if (chatItem.chatDir.type !== "groupRcv") continue
|
||||
const sender = chatItem.chatDir.groupMember
|
||||
|
||||
const isCustomer = sender.memberId === groupInfo.businessChat.customerId
|
||||
const isTeamMember = state.type === "teamPending" || state.type === "teamLocked"
|
||||
? sender.groupMemberId === state.teamMemberGId
|
||||
: false
|
||||
const isGrok = (state.type === "grokMode" || state.type === "teamPending")
|
||||
&& state.grokMemberGId === sender.groupMemberId
|
||||
|
||||
if (isGrok) continue // skip Grok messages (we sent them via grokChat)
|
||||
if (isCustomer) onCustomerMessage(groupId, groupInfo, chatItem, state)
|
||||
else if (isTeamMember) onTeamMemberMessage(groupId, state)
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Bot Initialization
|
||||
|
||||
In `index.ts`:
|
||||
1. Parse config
|
||||
2. Connect both ChatClients (`ChatClient.create("ws://localhost:PORT")`)
|
||||
3. Verify both have active user profiles
|
||||
4. Register bot commands (`/grok`, `/team`) and set `peerType: "bot"` via `apiUpdateProfile`
|
||||
5. Ensure main bot has business address with auto-accept and welcome auto-reply
|
||||
6. Start Grok agent event loop (background, same process)
|
||||
7. Start main bot event loop
|
||||
|
||||
Business address setup:
|
||||
**Text extraction:**
|
||||
```typescript
|
||||
await mainClient.enableAddressAutoAccept(
|
||||
mainUser.userId,
|
||||
{ type: "text", text: welcomeMessage(config.groupLinks) },
|
||||
true // businessAddress = true
|
||||
)
|
||||
```
|
||||
|
||||
The auto-reply handles Step 1 (Welcome) automatically on connect.
|
||||
|
||||
## 7. Event Processing
|
||||
|
||||
Main event loop in `SupportBot.run()` iterates `mainClient.msgQ`:
|
||||
|
||||
| Event | Action |
|
||||
|-------|--------|
|
||||
| `acceptingBusinessRequest` | Initialize conversation state as `welcome` |
|
||||
| `newChatItems` (group, business, customer rcv) | Dispatch to `onCustomerMessage` |
|
||||
| `newChatItems` (group, business, team member rcv) | Dispatch to `onTeamMemberMessage` |
|
||||
| `leftMember` / `deletedMemberUser` / `groupDeleted` | Clean up conversation state |
|
||||
| Everything else | Ignore |
|
||||
|
||||
**Identifying senders in `newChatItems`:**
|
||||
- `chatItem.chatDir.type === "groupRcv"` → received from `chatItem.chatDir.groupMember`
|
||||
- `chatItem.chatDir.type === "groupSnd"` → our own message (skip)
|
||||
- Customer: `chatDir.groupMember.memberId === groupInfo.businessChat.customerId`
|
||||
- Team member: `chatDir.groupMember.groupMemberId === state.teamMemberGId`
|
||||
- Grok agent: `chatDir.groupMember.groupMemberId === state.grokMemberGId` (skip — we sent it via grokClient)
|
||||
|
||||
**Extracting text:**
|
||||
```typescript
|
||||
function extractText(content: CIContent): string | null {
|
||||
if (content.type === "rcvMsgContent" || content.type === "sndMsgContent") {
|
||||
const mc = content.msgContent
|
||||
if (mc.type === "text" || mc.type === "link" || mc.type === "file") return mc.text
|
||||
}
|
||||
return null
|
||||
function extractText(chatItem: T.ChatItem): string | null {
|
||||
const text = util.ciContentText(chatItem)
|
||||
return text?.trim() || null
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Message Routing
|
||||
## 9. Message Routing Table
|
||||
|
||||
`onCustomerMessage(groupId, groupInfo, text, state)`:
|
||||
`onCustomerMessage(groupId, groupInfo, chatItem, state)`:
|
||||
|
||||
| State | `/grok` | `/team` | Other text |
|
||||
|-------|---------|---------|------------|
|
||||
| `welcome` | — | — | Forward to team, reply with queue msg → `teamQueue` (store msg) |
|
||||
| `teamQueue` | Activate Grok (with accumulated msgs) → `grokMode` | Activate team → `teamPending` | Forward to team, append to `userMessages` |
|
||||
| `grokMode` | (ignored, already grok) | Activate team → `teamPending` | Forward to Grok API + team |
|
||||
| `teamPending` (grok present) | Forward to Grok | (ignored, already team) | No forwarding (team sees directly) |
|
||||
| `teamPending` (no grok) | Reply "team mode" | — | No forwarding (team sees directly) |
|
||||
| `teamLocked` | Reply "team mode" | — | No forwarding needed (team sees directly) |
|
||||
| State | Input | Actions | API Calls | Next State |
|
||||
|-------|-------|---------|-----------|------------|
|
||||
| `welcome` | any text | Forward to team, send queue reply | `mainChat.apiSendTextMessage([Group, teamGroupId], fwd)` + `mainChat.apiSendTextMessage([Group, groupId], queueMsg)` | `teamQueue` (store msg) |
|
||||
| `teamQueue` | `/grok` | Activate Grok (invite, wait join, send accumulated msgs to Grok API, relay response) | `mainChat.apiAddMember(groupId, grokContactId, "member")` + `mainChat.apiSendTextMessage([Group, groupId], grokActivatedMsg)` + wait for join + `grokChat.apiSendTextMessage([Group, grokLocalGId], grokResponse)` | `grokMode` |
|
||||
| `teamQueue` | `/team` | Add team member | `mainChat.apiAddMember(groupId, teamContactId, "member")` + `mainChat.apiSendTextMessage([Group, groupId], teamAddedMsg)` | `teamPending` |
|
||||
| `teamQueue` | other text | Forward to team, append to userMessages | `mainChat.apiSendTextMessage([Group, teamGroupId], fwd)` | `teamQueue` |
|
||||
| `grokMode` | `/grok` | Ignore (already in grok mode) | — | `grokMode` |
|
||||
| `grokMode` | `/team` | Add team member (keep Grok for now) | `mainChat.apiAddMember(groupId, teamContactId, "member")` + `mainChat.apiSendTextMessage([Group, groupId], teamAddedMsg)` | `teamPending` (carry grokMemberGId + history) |
|
||||
| `grokMode` | other text | Forward to Grok API + team | Grok API call + `grokChat.apiSendTextMessage([Group, grokLocalGId], response)` + `mainChat.apiSendTextMessage([Group, teamGroupId], fwd)` | `grokMode` (append history) |
|
||||
| `teamPending` (grok present) | `/grok` | Forward to Grok (still usable) | Grok API call + `grokChat.apiSendTextMessage(...)` | `teamPending` |
|
||||
| `teamPending` (no grok) | `/grok` | Reply "team mode" | `mainChat.apiSendTextMessage([Group, groupId], teamLockedMsg)` | `teamPending` |
|
||||
| `teamPending` | `/team` | Ignore (already team) | — | `teamPending` |
|
||||
| `teamPending` | other text | No forwarding (team sees directly in group) | — | `teamPending` |
|
||||
| `teamLocked` | `/grok` | Reply "team mode" | `mainChat.apiSendTextMessage([Group, groupId], teamLockedMsg)` | `teamLocked` |
|
||||
| `teamLocked` | `/team` | Ignore | — | `teamLocked` |
|
||||
| `teamLocked` | other text | No action (team sees directly) | — | `teamLocked` |
|
||||
|
||||
## 9. Team Forwarding
|
||||
## 10. Team Forwarding
|
||||
|
||||
Forward messages to the designated team group:
|
||||
```typescript
|
||||
async forwardToTeam(groupId: number, groupInfo: GroupInfo, text: string): Promise<void> {
|
||||
const customerName = groupInfo.groupProfile.displayName || `group-${groupId}`
|
||||
const fwd = `[${customerName} #${groupId}]\n${text}`
|
||||
await this.mainClient.apiSendTextMessage(ChatType.Group, this.config.teamGroupId, fwd)
|
||||
async forwardToTeam(groupId: number, groupInfo: T.GroupInfo, text: string): Promise<void> {
|
||||
const name = groupInfo.groupProfile.displayName || `group-${groupId}`
|
||||
const fwd = `[${name} #${groupId}]\n${text}`
|
||||
await this.mainChat.apiSendTextMessage(
|
||||
[T.ChatType.Group, this.config.teamGroup.id],
|
||||
fwd
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Adding team member to business chat:
|
||||
```typescript
|
||||
async activateTeam(groupId: number, currentState: ConversationState): Promise<void> {
|
||||
const teamContactId = this.config.teamMemberContactIds[0]
|
||||
const member = await this.mainClient.apiAddMember(groupId, teamContactId, "member")
|
||||
async activateTeam(groupId: number, state: ConversationState): Promise<void> {
|
||||
const teamContactId = this.config.teamMembers[0].id // round-robin or first available
|
||||
const member = await this.mainChat.apiAddMember(groupId, teamContactId, "member")
|
||||
this.conversations.set(groupId, {
|
||||
type: "teamPending",
|
||||
teamMemberGId: member.groupMemberId,
|
||||
grokMemberGId: currentState.type === "grokMode" ? currentState.grokMemberGId : undefined,
|
||||
history: currentState.type === "grokMode" ? currentState.history : undefined,
|
||||
grokMemberGId: state.type === "grokMode" ? state.grokMemberGId : undefined,
|
||||
history: state.type === "grokMode" ? state.history : undefined,
|
||||
})
|
||||
await this.sendGroupMessage(groupId, teamAddedMessage(this.config.timezone))
|
||||
await this.mainChat.apiSendTextMessage(
|
||||
[T.ChatType.Group, groupId],
|
||||
teamAddedMessage(this.config.timezone)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Grok API Integration
|
||||
|
||||
**`grok.ts`** wraps xAI's OpenAI-compatible API:
|
||||
- Endpoint: `https://api.x.ai/v1/chat/completions`
|
||||
- Model: `grok-3`
|
||||
- System prompt: "Privacy expert and SimpleX Chat evangelist" + curated docs from `docs/simplex-context.md`
|
||||
- Per-conversation history: last 20 messages (trim from front)
|
||||
- API key from `GROK_API_KEY` env var
|
||||
## 11. Grok API Integration
|
||||
|
||||
```typescript
|
||||
class GrokApiClient {
|
||||
private docsContext: string // loaded from file at startup
|
||||
constructor(private apiKey: string, private docsContext: string) {}
|
||||
|
||||
async chat(history: GrokMessage[], userMessage: string): Promise<string> {
|
||||
const messages = [
|
||||
{ role: "system", content: this.systemPrompt() },
|
||||
{role: "system", content: this.systemPrompt()},
|
||||
...history.slice(-20),
|
||||
{ role: "user", content: userMessage },
|
||||
{role: "user", content: userMessage},
|
||||
]
|
||||
const resp = await fetch("https://api.x.ai/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
|
||||
body: JSON.stringify({ model: "grok-3", messages, max_tokens: 2048 }),
|
||||
headers: {"Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`},
|
||||
body: JSON.stringify({model: "grok-3", messages, max_tokens: 2048}),
|
||||
})
|
||||
if (!resp.ok) throw new Error(`Grok API: ${resp.status}`)
|
||||
if (!resp.ok) throw new Error(`Grok API ${resp.status}: ${await resp.text()}`)
|
||||
const data = await resp.json()
|
||||
return data.choices[0].message.content
|
||||
}
|
||||
|
||||
private systemPrompt(): string {
|
||||
return `You are a privacy expert and SimpleX Chat evangelist...\n\n${this.docsContext}`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Activating Grok:**
|
||||
1. `apiAddMember(groupId, grokContactId, "member")` — invites Grok to business chat
|
||||
2. Store `pendingGrokJoins` mapping
|
||||
3. Wait for Grok agent to join (poll `grokGroupMap` with timeout)
|
||||
4. Build initial Grok history from `state.userMessages` (accumulated in teamQueue)
|
||||
5. Forward all accumulated messages to Grok API in a single call
|
||||
6. Send bot activation message first, then Grok response via `grokClient.apiSendTextMessage`
|
||||
7. State transitions to `grokMode`
|
||||
**Activating Grok** (on `/grok` in teamQueue):
|
||||
1. `mainChat.apiAddMember(groupId, grokContactId, "member")` → stores `pendingGrokJoins.set(member.memberId, groupId)`
|
||||
2. Send bot activation message: `mainChat.apiSendTextMessage([Group, groupId], grokActivatedMsg)`
|
||||
3. Wait for Grok join: poll `grokGroupMap.has(groupId)` with 30s timeout (or use `mainChat.wait("connectedToGroupMember", pred, 30000)`)
|
||||
4. Build initial Grok history from `state.userMessages`
|
||||
5. Call Grok API with accumulated messages
|
||||
6. Send response via Grok identity: `grokChat.apiSendTextMessage([Group, grokGroupMap.get(groupId)!], response)`
|
||||
7. Transition to `grokMode` with history
|
||||
|
||||
**Fallback**: If Grok API fails, send error message and revert to teamQueue state.
|
||||
**Fallback:** If Grok API fails → send error message via `mainChat.apiSendTextMessage`, keep accumulated messages, stay in `teamQueue`.
|
||||
|
||||
## 11. One-Way Gate Logic
|
||||
|
||||
Per spec: "/grok permanently disabled ONLY after team joins AND team member sends a message."
|
||||
## 12. One-Way Gate Logic
|
||||
|
||||
```typescript
|
||||
async onTeamMemberMessage(groupId: number, state: ConversationState): Promise<void> {
|
||||
@@ -303,110 +364,187 @@ async onTeamMemberMessage(groupId: number, state: ConversationState): Promise<vo
|
||||
|
||||
// Remove Grok if present
|
||||
if (state.grokMemberGId) {
|
||||
try { await this.mainClient.apiRemoveMembers(groupId, [state.grokMemberGId]) } catch {}
|
||||
try { await this.mainChat.apiRemoveMembers(groupId, [state.grokMemberGId]) } catch {}
|
||||
grokGroupMap.delete(groupId)
|
||||
reverseGrokMap.delete(/* grokLocalGroupId */)
|
||||
}
|
||||
|
||||
// Lock: /grok permanently disabled
|
||||
this.conversations.set(groupId, { type: "teamLocked", teamMemberGId: state.teamMemberGId })
|
||||
this.conversations.set(groupId, {type: "teamLocked", teamMemberGId: state.teamMemberGId})
|
||||
}
|
||||
```
|
||||
|
||||
Timeline:
|
||||
1. User sends `/team` → bot adds team member → state = `teamPending` (Grok still usable if present)
|
||||
2. Team member sends a message → state = `teamLocked`, Grok removed
|
||||
3. Any subsequent `/grok` → "You are now in team mode. A team member will reply to your message."
|
||||
Timeline per spec:
|
||||
1. User sends `/team` → `apiAddMember` → state = `teamPending` (Grok still usable if present)
|
||||
2. Team member sends message → `onTeamMemberMessage` → state = `teamLocked`, Grok removed via `apiRemoveMembers`
|
||||
3. Any `/grok` → reply "You are now in team mode. A team member will reply to your message."
|
||||
|
||||
## 12. Bot Commands Setup
|
||||
|
||||
Register `/grok` and `/team` via profile update:
|
||||
## 13. Message Templates (verbatim from spec)
|
||||
|
||||
```typescript
|
||||
const botCommands: T.ChatBotCommand[] = [
|
||||
{ type: "command", keyword: "grok", label: "Ask Grok AI" },
|
||||
{ type: "command", keyword: "team", label: "Switch to team" },
|
||||
]
|
||||
await mainClient.apiUpdateProfile(mainUser.userId, {
|
||||
displayName, fullName, shortDescr, image, contactLink,
|
||||
peerType: T.ChatPeerType.Bot,
|
||||
preferences: { ...preferences, commands: botCommands },
|
||||
})
|
||||
// Welcome (auto-reply via business address)
|
||||
function welcomeMessage(groupLinks: string): string {
|
||||
return `Hello! Feel free to ask any question about SimpleX Chat.\n*Only SimpleX Chat team has access to your messages.* This is a SimpleX Chat team bot — it is not any LLM or AI.${groupLinks ? `\n*Join public groups*: ${groupLinks}` : ""}\nPlease send questions in English, you can use translator.`
|
||||
}
|
||||
|
||||
// After first message (teamQueue)
|
||||
function teamQueueMessage(timezone: string): string {
|
||||
const hours = isWeekend(timezone) ? "48" : "24"
|
||||
return `Thank you for your message, it is forwarded to the team.\nIt may take a team member up to ${hours} hours to reply.\n\nClick /grok if your question is about SimpleX apps or network, is not sensitive, and you want Grok LLM to answer it right away. *Your previous message and all subsequent messages will be forwarded to Grok* until you click /team. You can ask Grok questions in any language and it will not see your profile name.\n\nWe appreciate if you try Grok: you can learn a lot about SimpleX Chat from it. It is objective, answers the way our team would, and it saves our team time.`
|
||||
}
|
||||
|
||||
// Grok activated
|
||||
const grokActivatedMessage = `*You are now chatting with Grok. You can send questions in any language.* Your message(s) have been forwarded.\nSend /team at any time to switch to a human team member.`
|
||||
|
||||
// Team added
|
||||
function teamAddedMessage(timezone: string): string {
|
||||
const hours = isWeekend(timezone) ? "48" : "24"
|
||||
return `A team member has been added and will reply within ${hours} hours. You can keep describing your issue — they will see the full conversation.`
|
||||
}
|
||||
|
||||
// Team mode locked
|
||||
const teamLockedMessage = "You are now in team mode. A team member will reply to your message."
|
||||
```
|
||||
|
||||
## 13. Message Templates
|
||||
**Weekend detection:**
|
||||
```typescript
|
||||
function isWeekend(timezone: string): boolean {
|
||||
const day = new Intl.DateTimeFormat("en-US", {timeZone: timezone, weekday: "short"}).format(new Date())
|
||||
return day === "Sat" || day === "Sun"
|
||||
}
|
||||
```
|
||||
|
||||
All in `messages.ts`, verbatim from spec:
|
||||
## 14. Complete API Call Map (100% Coverage)
|
||||
|
||||
- **Welcome** (auto-reply): "Hello! Feel free to ask any question about SimpleX Chat..."
|
||||
- **Team queue** (after 1st msg): "Thank you for your message, it is forwarded to the team. It may take a team member up to {24/48} hours to reply. Click /grok if..."
|
||||
- **Grok activated**: "*You are now chatting with Grok...*"
|
||||
- **Team added**: "A team member has been added and will reply within {24/48} hours..."
|
||||
- **Team mode locked**: "You are now in team mode. A team member will reply to your message."
|
||||
| # | Operation | When | ChatApi Instance | Method | Parameters | Response Type | Error Handling |
|
||||
|---|-----------|------|-----------------|--------|------------|---------------|----------------|
|
||||
| 1 | Init main bot | Startup | mainChat | `bot.run()` (wraps `ChatApi.init`) | dbFilePrefix, profile, addressSettings | `[ChatApi, User, UserContactLink]` | Exit on failure |
|
||||
| 2 | Init Grok agent | Startup | grokChat | `ChatApi.init(grokDbPrefix)` | dbFilePrefix | `ChatApi` | Exit on failure |
|
||||
| 3 | Get/create Grok user | Startup | grokChat | `apiGetActiveUser()` / `apiCreateActiveUser(profile)` | profile: {displayName: "Grok AI"} | `User` | Exit on failure |
|
||||
| 4 | Start Grok chat | Startup | grokChat | `startChat()` | — | void | Exit on failure |
|
||||
| 5 | Validate team group | Startup | mainChat | `apiListGroups(userId)` | userId | `GroupInfo[]` | Exit if ID:name mismatch |
|
||||
| 6 | Validate contacts | Startup | mainChat | `apiListContacts(userId)` | userId | `Contact[]` | Exit if ID:name mismatch |
|
||||
| 7 | First-run: create link | First-run | mainChat | `apiCreateLink(userId)` | userId | `string` (invitation link) | Exit on failure |
|
||||
| 8 | First-run: connect | First-run | grokChat | `apiConnectActiveUser(invLink)` | connLink | `ConnReqType` | Exit on failure |
|
||||
| 9 | First-run: wait | First-run | mainChat | `wait("contactConnected", 60000)` | event, timeout | `ChatEvent \| undefined` | Exit on timeout |
|
||||
| 10 | Send msg to customer | Various | mainChat | `apiSendTextMessage([Group, groupId], text)` | chat, text | `AChatItem[]` | Log error |
|
||||
| 11 | Forward to team | welcome→teamQueue, teamQueue msg | mainChat | `apiSendTextMessage([Group, teamGroupId], fwd)` | chat, formatted text | `AChatItem[]` | Log error |
|
||||
| 12 | Invite Grok to group | /grok in teamQueue | mainChat | `apiAddMember(groupId, grokContactId, "member")` | groupId, contactId, role | `GroupMember` | Send error msg, stay in teamQueue |
|
||||
| 13 | Grok joins group | receivedGroupInvitation | grokChat | `apiJoinGroup(groupId)` | groupId | `GroupInfo` | Log error |
|
||||
| 14 | Grok sends response | After Grok API reply | grokChat | `apiSendTextMessage([Group, grokLocalGId], text)` | chat, text | `AChatItem[]` | Send error msg via mainChat |
|
||||
| 15 | Invite team member | /team | mainChat | `apiAddMember(groupId, teamContactId, "member")` | groupId, contactId, role | `GroupMember` | Send error msg to customer |
|
||||
| 16 | Remove Grok | Team member msg in teamPending | mainChat | `apiRemoveMembers(groupId, [grokMemberGId])` | groupId, memberIds | `GroupMember[]` | Ignore (may have left) |
|
||||
| 17 | Update bot profile | Startup (via bot.run) | mainChat | `apiUpdateProfile(userId, profile)` | userId, profile with peerType+commands | `UserProfileUpdateSummary` | Log warning |
|
||||
| 18 | Set address settings | Startup (via bot.run) | mainChat | `apiSetAddressSettings(userId, settings)` | userId, {businessAddress, autoAccept, welcomeMessage} | void | Exit on failure |
|
||||
|
||||
Weekend detection: `Intl.DateTimeFormat("en-US", { timeZone, weekday: "short" })` → "Sat"/"Sun".
|
||||
|
||||
## 14. Error Handling
|
||||
## 15. Error Handling
|
||||
|
||||
| Scenario | Handling |
|
||||
|----------|----------|
|
||||
| CLI disconnection | Event loop exits → log, process exits (let process manager restart) |
|
||||
| Grok API error | Catch, send "Grok temporarily unavailable", revert to teamQueue |
|
||||
| `apiAddMember` fails | Catch, send error message to user |
|
||||
| `apiRemoveMembers` fails | Ignore (member may have left already) |
|
||||
| Grok join timeout (30s) | Send "Grok unavailable", stay in current state |
|
||||
| Customer leaves group | Clean up conversation state on `leftMember` event |
|
||||
| Group deleted | Clean up on `groupDeleted` event |
|
||||
| Grok leaves during teamPending | Clear Grok reference, keep teamPending |
|
||||
| Team member leaves | Revert to teamQueue |
|
||||
| ChatApi init fails | Log error, exit (let process manager restart) |
|
||||
| Grok API error (HTTP/timeout) | `mainChat.apiSendTextMessage` "Grok temporarily unavailable", revert to `teamQueue` |
|
||||
| `apiAddMember` fails (Grok) | `mainChat.apiSendTextMessage` error msg, stay in `teamQueue` |
|
||||
| `apiAddMember` fails (team) | `mainChat.apiSendTextMessage` error msg, stay in current state |
|
||||
| `apiRemoveMembers` fails | Catch and ignore (member may have left) |
|
||||
| Grok join timeout (30s) | `mainChat.apiSendTextMessage` "Grok unavailable", stay in `teamQueue` |
|
||||
| Customer leaves (`leftMember` where member is customer) | Delete conversation state, delete grokGroupMap entry |
|
||||
| Group deleted | Delete conversation state, delete grokGroupMap entry |
|
||||
| Grok leaves during `teamPending` | Clear `grokMemberGId` from state, keep `teamPending` |
|
||||
| Team member leaves | Revert to `teamQueue` (accumulate messages again) |
|
||||
| Bot removed from group (`deletedMemberUser`) | Delete conversation state |
|
||||
| Grok agent connection lost | Log error; Grok features unavailable until restart |
|
||||
| `apiSendTextMessage` fails | Log error, continue (message lost but bot stays alive) |
|
||||
| Config validation fails | Print descriptive error with actual vs expected name, exit |
|
||||
|
||||
## 15. Implementation Sequence
|
||||
## 16. Implementation Sequence
|
||||
|
||||
**Phase 1: Scaffold** — `package.json`, `tsconfig.json`, `config.ts`, `index.ts`, `util.ts`
|
||||
- Create project, install deps, implement config parsing
|
||||
- Connect both ChatClients, verify user profiles, set up business address
|
||||
- Verify: both clients connect and print profiles
|
||||
**Phase 1: Scaffold**
|
||||
- Create project: `package.json`, `tsconfig.json`
|
||||
- Implement `config.ts`: CLI arg parsing, ID:name format, `Config` type
|
||||
- Implement `index.ts`: init both ChatApi instances, verify profiles
|
||||
- Implement `util.ts`: `isWeekend`, logging
|
||||
- **Verify:** Both instances init, print user profiles, validate config
|
||||
|
||||
**Phase 2: State machine + event loop** — `state.ts`, `bot.ts`, `messages.ts`
|
||||
- Define `ConversationState` type
|
||||
- Implement `SupportBot` class with event loop
|
||||
- Handle `acceptingBusinessRequest` → init state
|
||||
- Handle `newChatItems` → customer message dispatch
|
||||
- Implement Welcome → TeamQueue transition + team forwarding
|
||||
- Verify: customer connects, sends msg, bot replies with queue info, msg forwarded
|
||||
**Phase 2: State machine + event loop**
|
||||
- Implement `state.ts`: `ConversationState` union type
|
||||
- Implement `bot.ts`: `SupportBot` class with `conversations` map
|
||||
- Handle `acceptingBusinessRequest` → init state as `welcome`
|
||||
- Handle `newChatItems` → sender identification → customer message dispatch
|
||||
- Implement welcome → teamQueue transition + team forwarding
|
||||
- Implement `messages.ts`: all templates
|
||||
- **Verify:** Customer connects → welcome auto-reply → sends msg → forwarded to team group → queue reply received
|
||||
|
||||
**Phase 3: Grok integration** — `grok.ts`, updates to `bot.ts`
|
||||
- Implement `GrokApiClient` with system prompt + doc injection
|
||||
- Implement Grok agent event loop (auto-join groups)
|
||||
- Implement `activateGrok`: add member, ID mapping, activation message
|
||||
- Implement `forwardToGrok`: API call → response via Grok client
|
||||
- Verify: /grok works, user sees Grok responses under Grok profile
|
||||
**Phase 3: Grok integration**
|
||||
- Implement `grok.ts`: `GrokApiClient` with system prompt + docs injection
|
||||
- Implement Grok agent event handler (`receivedGroupInvitation` → auto-join)
|
||||
- Implement `activateGrok`: add member, ID mapping, wait for join, Grok API call, send response via grokChat
|
||||
- Implement `forwardToGrok`: ongoing message routing in grokMode
|
||||
- **Verify:** `/grok` → Grok joins as separate participant → Grok responses appear from Grok profile
|
||||
|
||||
**Phase 4: Team mode + one-way gate** — updates to `bot.ts`
|
||||
**Phase 4: Team mode + one-way gate**
|
||||
- Implement `activateTeam`: add team member
|
||||
- Implement `onTeamMemberMessage`: detect team msg → lock → remove Grok
|
||||
- Implement `/grok` rejection in `teamLocked`
|
||||
- Verify: full flow including gate lock
|
||||
- **Verify:** Full flow including: teamQueue → /grok → grokMode → /team → teamPending → team msg → teamLocked → /grok rejected
|
||||
|
||||
**Phase 5: Polish** — bot commands, edge cases, docs
|
||||
- Register /grok and /team as bot commands via profile update
|
||||
- Handle customer leave, group delete, Grok timeout, error messages
|
||||
- Write `simplex-context.md` for Grok prompt injection
|
||||
- End-to-end test
|
||||
**Phase 5: Polish + first-run**
|
||||
- Implement `--first-run` auto-contact establishment
|
||||
- Handle edge cases: customer leave, group delete, Grok timeout, member leave
|
||||
- Write `docs/simplex-context.md` for Grok prompt injection
|
||||
- End-to-end test all flows
|
||||
|
||||
## 16. Verification
|
||||
## 17. Self-Review Requirement
|
||||
|
||||
1. **Start infrastructure**: Two `simplex-chat` CLIs on ports 5225/5226
|
||||
2. **Initialize profiles**: Create "Support Bot" and "Grok" user profiles, establish contact between them
|
||||
3. **Run bot**: `npx ts-node src/index.ts --team-group X --team-members Y --grok-contact-id Z`
|
||||
4. **Test welcome flow**: Connect from a third SimpleX client to bot's business address → verify welcome message
|
||||
5. **Test first message**: Send a question → verify forwarded to team group, queue reply received
|
||||
6. **Test /grok**: Send `/grok` → verify Grok joins as separate participant, responses appear from Grok profile
|
||||
7. **Test /team from grok**: Send `/team` → verify team member added, send team member message → verify /grok locked, Grok removed
|
||||
8. **Test weekend**: Change timezone to a weekend timezone, verify "48 hours" in messages
|
||||
**Mandatory for all implementation subagents:**
|
||||
|
||||
Each code artifact must undergo adversarial self-review/fix loop:
|
||||
1. Write/edit code
|
||||
2. Self-review against this plan: check correctness, completeness, consistency, all state transitions covered, all API calls match the plan, all error cases handled
|
||||
3. Fix any issues found
|
||||
4. Repeat review until **2 consecutive zero-issue passes**
|
||||
5. Only then report completion
|
||||
6. User reviews and provides feedback
|
||||
7. If changes needed → return to step 1 (review cycle restarts)
|
||||
8. Done when: 2 clean LLM passes AND user finds no issues
|
||||
|
||||
Any edit restarts the review cycle. Batch changes within a round.
|
||||
|
||||
## 18. Verification
|
||||
|
||||
**First-run setup:**
|
||||
```bash
|
||||
cd apps/simplex-chat-support-bot
|
||||
npm install
|
||||
npx ts-node src/index.ts --first-run --db-prefix ./data/bot --grok-db-prefix ./data/grok
|
||||
# → Prints: "Grok contact established. ContactId=X. Use: --grok-contact X:GrokAI"
|
||||
```
|
||||
|
||||
**Normal run:**
|
||||
```bash
|
||||
npx ts-node src/index.ts \
|
||||
--team-group 1:SupportTeam \
|
||||
--team-members 2:Alice,3:Bob \
|
||||
--grok-contact 4:GrokAI \
|
||||
--timezone America/New_York \
|
||||
--group-links "https://simplex.chat/contact#..."
|
||||
```
|
||||
|
||||
**Test scenarios:**
|
||||
1. Connect from SimpleX client to bot's business address → verify welcome message
|
||||
2. Send question → verify forwarded to team group with `[CustomerName #groupId]` prefix, queue reply received
|
||||
3. Send `/grok` → verify Grok joins as separate participant, responses appear from "Grok AI" profile
|
||||
4. Send text in grokMode → verify Grok response + forwarded to team
|
||||
5. Send `/team` → verify team member added, team added message
|
||||
6. Send team member message → verify Grok removed, state locked
|
||||
7. Send `/grok` after lock → verify "team mode" reply
|
||||
8. Test weekend: set timezone to weekend timezone → verify "48 hours" in messages
|
||||
9. Customer disconnects → verify state cleanup
|
||||
10. Grok API failure → verify error message, graceful fallback to teamQueue
|
||||
|
||||
### Critical Reference Files
|
||||
- `packages/simplex-chat-client/typescript/src/client.ts` — ChatClient API
|
||||
- `packages/simplex-chat-client/types/typescript/src/events.ts` — Event types (CEvt)
|
||||
- `packages/simplex-chat-client/types/typescript/src/types.ts` — Domain types
|
||||
- `packages/simplex-chat-client/types/typescript/src/commands.ts` — Command builders (CC)
|
||||
- `packages/simplex-chat-client/typescript/examples/squaring-bot.js` — Reference pattern
|
||||
|
||||
- **Native library API:** `packages/simplex-chat-nodejs/src/api.ts` (ChatApi class — all methods)
|
||||
- **Bot automation:** `packages/simplex-chat-nodejs/src/bot.ts` (bot.run — setup helper)
|
||||
- **Utilities:** `packages/simplex-chat-nodejs/src/util.ts` (ciContentText, ciBotCommand, chatInfoRef)
|
||||
- **Types:** `packages/simplex-chat-client/types/typescript/src/types.ts` (BusinessChatInfo, GroupMember, CIDirection, etc.)
|
||||
- **Events:** `packages/simplex-chat-client/types/typescript/src/events.ts` (CEvt — all event types)
|
||||
- **Product spec:** `apps/multiplatform/plans/20260207-support-bot.md`
|
||||
|
||||
Reference in New Issue
Block a user