This page shows common bot patterns. Runnable projects are available under
examples/.
| Example | Purpose |
|---|---|
| basic-bot | Connect to a room, read chat, reply to mentions, and move near users. |
| voice-bot | Play a WAV file into a room and record remote PCM audio. |
| voice-ai-bot | Connect voice input and output to Gemini Live. |
| speech-to-speech-bot | Speech recognition, LLM response generation, and text-to-speech playback. |
| dify-bot | Forward room chat to a Dify application. |
| bt-bot | Autonomous bot driven by a behavior tree and an LLM, with chat and voice (README in Japanese). |
const botInfo = await client.getInfo()
client.chat.onMessage(async ({ from, text, mention, reply }) => {
if (mention?.sessionId !== botInfo.sessionId) return
await reply(`Hello ${from.name ?? 'there'}. I received: ${text}`)
})
client.chat.onMessage(async ({ text, reply }) => {
const [command, ...args] = text.trim().split(/\s+/)
switch (command) {
case '/walk':
await client.avatar.play({ id: 'walking', loop: false })
await reply('Walking animation requested.')
break
case '/move': {
const [x, y, z] = args.map(Number)
if ([x, y, z].some(Number.isNaN)) {
await reply('Usage: /move <x> <y> <z>')
return
}
await client.avatar.moveTo({ x, y, z })
await reply(`Moved to ${x}, ${y}, ${z}.`)
break
}
}
})
const followNearestUser = async () => {
const [target] = await client.room.getNearbyUsers(10)
const current = client.avatar.getPosition()
if (!target?.position || !current) return
await client.avatar.lookAt(target.position)
await client.avatar.moveTo({
x: target.position.x + 1.5,
y: target.position.y,
z: target.position.z + 1.5,
})
}
const followLoop = async () => {
try {
await followNearestUser()
} catch (error) {
console.error('Failed to follow nearest user:', error)
} finally {
setTimeout(followLoop, 1000)
}
}
void followLoop()
const users = await client.room.getUsers()
for (const user of users) {
console.log({
id: user.id,
name: user.name,
position: user.position,
})
}
import { enableVoice } from '@metatell/bot-sdk'
// 48000 Hz, mono, signed 16-bit PCM. Use pcm.resample() for other rates.
const voice = await enableVoice(client, {
transport: { type: 'livekit' },
handlers: {
getLocalPcmStream: async function* () {
while (true) {
yield new Int16Array(960)
await new Promise((resolve) => setTimeout(resolve, 20))
}
},
},
})
await voice.detach()
const frames: Int16Array[] = []
await enableVoice(client, {
transport: { type: 'livekit' },
handlers: {
onRemotePcm: async (pcm, meta) => {
console.log('audio from', meta.fromIdentity)
frames.push(new Int16Array(pcm))
},
},
})
client.on('voice:mute-changed', ({ muted }) => {
console.log('muted:', muted)
})
await client.muteVoice?.(true)
Before voice is enabled, muteVoice() updates the client state, emits
voice:mute-changed, and makes sendVoiceFrame() ignore frames. While an
enableVoice() attachment is active, muteVoice() is routed to the transport
instead, and the LiveKit transport does not currently mute published audio.
Stop your local PCM stream if the bot must go silent. AgentClient also exposes
isVoiceMuted().
Use the Dify example when a bot should forward metatell chat to a Dify application and post the application response back to the room.
cd examples/dify-bot
npm ci
npm run build
cp .env.example .env
npm start -- https://metatell.app/YOUR_ROOM_ID