Sui Stack Messaging SDK: EP 3 Interacting with AI BOT
Build an AI Chatbot with Groq on Sui Encrypted Messaging

มาถึง EP สุดท้ายแล้วของ series Sui Stack Messaging SDK ละครับ ซึ่ง EP นี้ไม่ยากละ เพราะจะเอาองค์รวมความรู้ทั้งหมดของ EP ก่อนหน้านี้มาเขียนอีกครั้งนึง เพื่อให้ AI เราตอบโต้กับเราได้ ซึ่งอย่างที่บอกไป AI ให้มองแยกเป็น interface นึง ส่วนวิธีการติดต่อก็จะเหมือนเดิมเลยครับ ใครยังไม่ตามลองไปทำ 2 EP ก่อนหน้าก่อนนะ
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 (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 ก่อน
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
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
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 ตามตัวอย่างด้านบนอีกรอบนึงคือ
ครั้งแรก bot จะดูว่า channel มีข้อความกี่ข้อความแล้ว แล้วจำตัวเลขนี้ไว้ ข้อความก่อนหน้านี้จะถูกข้ามทั้งหมด
ครั้งถัดไป bot ถาม SDK ว่ามีข้อความใหม่กว่าตัวเลขที่จำไว้ไหม ถ้ามีก็ดึงมา
กรองข้อความ ข้ามข้อความที่ bot ส่งเอง ไม่งั่นได้ติดในลูปแน่
ส่งให้ AI เรียก
getAIResponse()พร้อม conversation historyตัดความยาว
truncateToBytes()ตัดไม่ให้เกิน 480 bytes ซึ่งจะเป็นส่วนช่วยป้องกัน error นั้นเองส่งกลับ
executeSendMessageTransaction()encrypt + store on-chainรอ finality
waitForTransaction()รอให้ transaction เสร็จก่อน poll รอบถัดไปจำบริบท เก็บ history ไว้ให้ AI ตอบครั้งหน้าได้ต่อเนื่อง
ฟังก์ชัน truncateToBytes — ตัดข้อความให้พอดี 512 bytes
เราต้องมีฟังก์ชันนี้เป็น safety net อีกทึนึงเพราะแม้จะบอก AI ให้ตอบสั้นแล้ว บางทีมันก็ยังตอบยาวเกินอยู่ดี ใครมีเทคนิดบีบคอให้ AI ไม่งอแงบอกหน่อยนะ ชอบตอบเกินความจำเป็น
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 เพื่อเริ่มต้นการทำงาน
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 เข้าห้องที่เราสร้างนั้นเอง
# 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 นึงขึ้นมาเพื่อรันบอท
npm run bot
เรียบร้อยจ้าเสร็จละ ซึ่งแต่ละข้อความอาจจะต้องรอประมาณ 5-10 วินาที bot จะตอบกลับมาอัตโนมัติ ทุกข้อความ encrypted on-chain ทั้งหมด ลองไปดู transaction บน https://suiscan.xyz/ ได้เลยนะ
ใครขี้เกียจก็ไปตามได้ที่นี้เลย เอา source code ทั้งหมดไว้ละ https://github.com/Contribution-DAO/ON-THE-MOVE-Workshop/tree/main/sui-messaging-sdk
แล้วเจอกันในบทความหน้านะ แอบกระซิบว่าเร็วๆนี้ในไทยจะมีงานที่โพกัสเกี่ยวกับ SUI Developer ด้วยน๊าา ชาร์ตเเบตรอได้เลยจ้า




