# Sui Stack Messaging SDK: EP 3 Interacting with AI BOT

มาถึง EP สุดท้ายแล้วของ series Sui Stack Messaging SDK ละครับ ซึ่ง EP นี้ไม่ยากละ เพราะจะเอาองค์รวมความรู้ทั้งหมดของ EP ก่อนหน้านี้มาเขียนอีกครั้งนึง เพื่อให้ AI เราตอบโต้กับเราได้ ซึ่งอย่างที่บอกไป AI ให้มองแยกเป็น interface นึง ส่วนวิธีการติดต่อก็จะเหมือนเดิมเลยครับ ใครยังไม่ตามลองไปทำ 2 EP ก่อนหน้าก่อนนะ

*   [https://onthemove.contributiondao.com/sui-stack-messaging-sdk-introduction](https://onthemove.contributiondao.com/sui-stack-messaging-sdk-introduction) (EP 1)
    
*   [https://onthemove.contributiondao.com/sui-stack-messaging-sdk-ep-2-first-step-encrypted-chat-cli](https://onthemove.contributiondao.com/sui-stack-messaging-sdk-ep-2-first-step-encrypted-chat-cli) (EP 2)
    

สิ่งที่เราจะต้องเตรียมคือ

*   Wallet สำหรับ AI
    
*   AI API KEY ขึ้นกับถนัดนะ ใครอยากใช้ตัวไหนก็ตามสบายเลย
    

ซึ่งก่อนหน้านี้เราสร้าง CLI chatroom ที่ส่งข้อความ encrypted ได้แล้วใช่ไหม วันนี้จะมาเพิ่มสิ่งที่ทำให้มันเจ๋งขึ้นอีกขั้น โดยจะทำให้ AI Bot ที่คอยตอบแชทอัตโนมัติ ทุกข้อความที่ bot ส่งก็ encrypted on-chain เหมือนกัน.. หลักการมันไม่มีอะไร แค่มี AI เป็น interface แค่นั้นเอง มาเริ่มกันเลย

## 1\. AI Wrapper

เราเลือกใช้ Groq API นะ ไปสร้าง API Key กันได้เลย ฟรี โดยเราจะเริ่มจากสร้าง ตัวกลางระหว่าง bot กับ Groq API ก่อน

```typescript
import Groq from "groq-sdk";
import { config } from "../config.js";

const groq = new Groq({ apiKey: config.groqApiKey });

const SYSTEM_PROMPT = `You are a helpful AI assistant in a Web3 chatroom
built on Sui blockchain. Knowledge: Sui, Move, DeFi, Walrus, Seal.

CRITICAL: Messages on-chain cap at 512 bytes.
Thai = 3 bytes/char → keep Thai replies under ~150 chars.
English → under ~400 chars. Be concise, 1-3 sentences max.
Respond in the same language the user writes in.`;

export async function getAIResponse(
  userMessage: string,
  conversationHistory: { role: "user" | "assistant"; content: string }[] = []
): Promise<string> {
  try {
    const messages = [
      { role: "system" as const, content: SYSTEM_PROMPT },
      ...conversationHistory.slice(-10), 
      { role: "user" as const, content: userMessage },
    ];

    const response = await groq.chat.completions.create({
      model: config.groqModel,
      max_tokens: 150,
      messages,
    });

    return response.choices[0]?.message?.content || "🤖 (no response)";
  } catch (err: any) {
    console.error("Groq API error:", err.message);
    return "🤖 Sorry, I'm having trouble connecting right now.";
  }
}
```

มีจุดที่ต้องเข้าใจหลักๆ คือ

*   **SYSTEM\_PROMPT** บอก AI ว่ามันคือใคร ตอบเรื่องอะไรได้บ้าง และที่สำคัญคือ**ต้องตอบสั้นๆ** เพราะ channel contract จำกัด message ไว้ที่ 512 bytes ภาษาไทย 1 ตัวอักษร = 3 bytes (UTF-8) ดังนั้นถ้าตอบยาวเกินจะส่งขึ้น chain ไม่ได้เลย error ตลอด อันนี้เเหละที่นั่ง debug อยู่นาน :'(
    
*   **conversationHistory** เราส่ง 10 ข้อความล่าสุดเข้าไปด้วย ทำให้ bot จำบริบทการสนทนาได้ ไม่ใช่ตอบแบบลืมหมดทุกครั้ง
    
*   **max\_tokens: 150** จำกัดความยาว reply ของ AI ไว้อีกชั้นหนึ่งจะได้ไม่หลุด
    

ส่วนใครถนัดตัวไหนก็ลองกับตัวนั้นได้เลย ส่วนนี้ไม่มีอะไรเเค่เขียนติดต่อกับ AI

## 2\. Bot Main Loop

ส่วนหลักของ EP คือBot ทำงานเป็น **long-running process** มาดู flow ทั้งหมดตั้งแต่เริ่มต้นจนตอบข้อความ

สร้างไฟล์ `bot.ts`

```typescript
import chalk from "chalk";
import { createBotClient } from "../client.js";
import { config } from "../config.js";
import { getAIResponse } from "./ai.js";
import { getEncryptionKey } from "../encryption.js";

const conversationHistory: Record<string, { role: "user" | "assistant"; content: string }[]> = {};
const pollingStates: Record<string, {
  lastMessageCount: bigint;
  lastCursor: bigint | null;
  channelId: string;
}> = {};
const encryptionKeyCache: Record<string, Awaited<ReturnType<typeof getEncryptionKey>>> = {};
```

เริ่มจาก state ที่เก็บใน memory ทั้งหมด ซึ่งจะมี `conversationHistory` สำหรับให้ AI จำบริบทได้, `pollingStates` สำหรับจำว่า poll ถึงข้อความไหนแล้ว, `encryptionKeyCache` สำหรับ cache key ไม่ต้อง fetch ทุกรอบ

### ฟังก์ชัน pollAndReply ใช้เพื่อ รับข้อความ → ถาม AI → ตอบกลับ

นี่คือหัวใจจริงๆ ของ bot ทำงาน 3 ขั้นตอนคือ ดึงข้อความใหม่ → ส่งให้ Groq ตอบ → ส่ง reply กลับเข้า channel

```typescript
async function pollAndReply(channelId: string, memberCapId: string) {
  const { client, messagingClient, keypair } = createBotClient();
  const botAddress = keypair.toSuiAddress();

  if (!encryptionKeyCache[channelId]) {
    encryptionKeyCache[channelId] = await getEncryptionKey(messagingClient, channelId, botAddress);
  }
  const encryptedKey = encryptionKeyCache[channelId];

  if (!pollingStates[channelId]) {
    const channels = await messagingClient.getChannelObjectsByChannelIds({
      channelIds: [channelId],
      userAddress: botAddress,
    });
    pollingStates[channelId] = {
      lastMessageCount: BigInt(channels[0].messages_count),
      lastCursor: null,
      channelId,
    };
    console.log(chalk.gray(`  Initialized polling for ${channelId.slice(0, 12)}...`));
    return;
  }

  const pollingState = pollingStates[channelId];
  const newMsgs = await messagingClient.getLatestMessages({
    channelId,
    userAddress: botAddress,
    pollingState,
  });

  if (newMsgs.messages.length === 0) return;

  for (const msg of newMsgs.messages) {
    if (msg.sender === botAddress) continue;

    console.log(chalk.cyan(`  📨 ${msg.sender.slice(0, 8)}...: ${msg.text}`));

    if (!conversationHistory[channelId]) conversationHistory[channelId] = [];

    const rawReply = await getAIResponse(msg.text, conversationHistory[channelId]);
    const aiReply = truncateToBytes(rawReply, 480);
    try {
      const { digest } = await messagingClient.executeSendMessageTransaction({
        signer: keypair,
        channelId,
        memberCapId,
        message: aiReply,
        encryptedKey,
      });
      await client.waitForTransaction({ digest });

      console.log(chalk.green(`  🤖 Bot: ${aiReply.slice(0, 80)}...`));
      console.log(chalk.gray(`     🔗 ${config.suiscanTxUrl(digest)}`));

      conversationHistory[channelId].push(
        { role: "user", content: msg.text },
        { role: "assistant", content: aiReply },
      );

      if (conversationHistory[channelId].length > 20) {
        conversationHistory[channelId] = conversationHistory[channelId].slice(-20);
      }
    } catch (err: any) {
      console.log(chalk.red(`  ❌ Failed to reply: ${err.message}`));
      delete encryptionKeyCache[channelId];
    }
  }
  pollingState.lastMessageCount += BigInt(newMsgs.messages.length);
}
```

เล่า flow ตามตัวอย่างด้านบนอีกรอบนึงคือ

1.  **ครั้งแรก** bot จะดูว่า channel มีข้อความกี่ข้อความแล้ว แล้วจำตัวเลขนี้ไว้ ข้อความก่อนหน้านี้จะถูกข้ามทั้งหมด
    
2.  **ครั้งถัดไป** bot ถาม SDK ว่ามีข้อความใหม่กว่าตัวเลขที่จำไว้ไหม ถ้ามีก็ดึงมา
    
3.  **กรองข้อความ** ข้ามข้อความที่ bot ส่งเอง ไม่งั่นได้ติดในลูปแน่
    
4.  **ส่งให้ AI** เรียก `getAIResponse()` พร้อม conversation history
    
5.  **ตัดความยาว** `truncateToBytes()` ตัดไม่ให้เกิน 480 bytes ซึ่งจะเป็นส่วนช่วยป้องกัน error นั้นเอง
    
6.  **ส่งกลับ** `executeSendMessageTransaction()` encrypt + store on-chain
    
7.  **รอ finality** `waitForTransaction()` รอให้ transaction เสร็จก่อน poll รอบถัดไป
    
8.  **จำบริบท** เก็บ history ไว้ให้ AI ตอบครั้งหน้าได้ต่อเนื่อง
    

### ฟังก์ชัน truncateToBytes — ตัดข้อความให้พอดี 512 bytes

เราต้องมีฟังก์ชันนี้เป็น safety net อีกทึนึงเพราะแม้จะบอก AI ให้ตอบสั้นแล้ว บางทีมันก็ยังตอบยาวเกินอยู่ดี ใครมีเทคนิดบีบคอให้ AI ไม่งอแงบอกหน่อยนะ ชอบตอบเกินความจำเป็น

```typescript
const MAX_MESSAGE_BYTES = 480;

function truncateToBytes(text: string, maxBytes: number): string {
  const bytes = new TextEncoder().encode(text);
  if (bytes.length <= maxBytes) return text;
  let cut = maxBytes - 3;
  while (cut > 0 && (bytes[cut] & 0xc0) === 0x80) cut--;
  return new TextDecoder().decode(bytes.slice(0, cut)) + "...";
}
```

ส่วนนี้เรากำหนดไว้ 480 เพราะถ้า 512 พอดีมันจะมีพวก overhead ข้อมูลอื่นๆด้วยทำให้เกินตลอด

### ฟังก์ชัน main เพื่อเริ่มต้นการทำงาน

```typescript
async function main() {
  const { messagingClient, keypair } = createBotClient();
  const botAddress = keypair.toSuiAddress();

  console.log(chalk.blue("🤖 Sui Chat AI Bot Starting..."));
  console.log(chalk.blue("   Bot wallet:"), botAddress);

  const result = await messagingClient.getChannelMemberships({ address: botAddress });
  const memberships = new Map<string, string>(
    result.memberships.map((m) => [m.channel_id, m.member_cap_id]),
  );

  if (memberships.size === 0) {
    console.log(chalk.yellow("  Bot is not a member of any channels."));
    console.log(chalk.white(`  Invite first: npm run dev -- invite <channelId> ${botAddress}`));
    process.exit(0);
  }

  console.log(chalk.green(`  Monitoring ${memberships.size} channel(s)`));

  let polling = false;
  const poll = async () => {
    if (polling) return;
    polling = true;
    try {
      for (const [channelId, memberCapId] of memberships) {
        await pollAndReply(channelId, memberCapId);
      }
    } finally {
      polling = false;
    }
  };

  await poll();
  setInterval(poll, config.botPollIntervalMs);
  console.log(chalk.green("  ✅ Bot is running. Press Ctrl+C to stop.\n"));
}

main().catch((err) => {
  console.error(chalk.red("Bot crashed:"), err);
  process.exit(1);
});
```

ตรง `polling` flag สำคัญ — ถ้ารอบก่อนยังทำไม่เสร็จ (เช่น Groq ตอบช้า หรือ Sui ยัง finalize ไม่เสร็จ) รอบถัดไปจะถูกข้ามไปเลย ป้องกัน object-lock error

## 3\. Demo

วิธีการทดสอบนั้นไม่ยากถ้าใครลองเล่น EP.2 ก็ให้เราสร้างห้องขึ้นมาก่อนแล้วทำการ invite bot เข้าห้องที่เราสร้างนั้นเอง

```bash
# Create room
npm run dev -- create-room

# Invite bot to join 
npm run dev -- invite <channelId> 0xBOT_ADDRESS

# Room own join the chat room
npm run dev -- chat <channelId>
```

หลังจากนั้นเปิดอีก terminal นึงขึ้นมาเพื่อรันบอท

```bash
npm run bot
```

เรียบร้อยจ้าเสร็จละ ซึ่งแต่ละข้อความอาจจะต้องรอประมาณ 5-10 วินาที bot จะตอบกลับมาอัตโนมัติ ทุกข้อความ encrypted on-chain ทั้งหมด ลองไปดู transaction บน https://suiscan.xyz/ ได้เลยนะ

![](https://cdn.hashnode.com/uploads/covers/68b7c67e9e994bcb96821cae/8347a0fb-543b-4a02-bef7-aae2e5422506.gif align="center")

ใครขี้เกียจก็ไปตามได้ที่นี้เลย เอา source code ทั้งหมดไว้ละ [https://github.com/Contribution-DAO/ON-THE-MOVE-Workshop/tree/main/sui-messaging-sdk](https://github.com/Contribution-DAO/ON-THE-MOVE-Workshop/tree/main/sui-messaging-sdk)

แล้วเจอกันในบทความหน้านะ แอบกระซิบว่าเร็วๆนี้ในไทยจะมีงานที่โพกัสเกี่ยวกับ SUI Developer ด้วยน๊าา ชาร์ตเเบตรอได้เลยจ้า
