How to Use Channel-Based Function Calling with Stream
This tutorial demonstrates how to implement channel-based function calling using the complete().stream() method in ChatBotKit. This approach is ideal for interactive conversations where you need real-time control over the conversation flow.
Learning Objectives
By the end of this tutorial, you will be able to:
- Use the
complete().stream()method for inline conversation processing - Handle streaming events directly in your code
- Implement static and channel-based function results
- Process the
waitForChannelMessageBeginevent for dynamic function execution - Display real-time tokens as they arrive
Prerequisites
- Node.js 18+ installed
- A ChatBotKit account with an API secret
- Basic understanding of async iterators and streams
Estimated time: 20 minutes
Understanding Stream vs Dispatch
| Aspect | complete().stream() | dispatch() |
|---|---|---|
| Execution | Inline, blocking | Background, async |
| Event source | Direct from stream iterator | Via channel.subscribe() |
| Use case | Interactive chat | Background tasks |
| Control | Direct in your loop | Requires separate subscription |
The complete().stream() method processes a conversation inline, streaming events directly to your code as they occur.
Step 1: Set Up Your Project
Create a new Node.js project and install the ChatBotKit SDK:
mkdir function-stream-example
cd function-stream-example
npm init -y
npm install @chatbotkit/sdk dotenv
Create a .env file with your API secret:
CHATBOTKIT_API_SECRET=your_api_secret_here
Step 2: Create the Basic Structure
Create a file called index.js:
import * as dotenv from 'dotenv'
import { ChatBotKit } from '@chatbotkit/sdk/index.js'
import { randomBytes } from 'node:crypto'
dotenv.config()
async function main() {
const client = new ChatBotKit({
secret: process.env.CHATBOTKIT_API_SECRET,
})
// Generate unique channel ID for dynamic functions
const weatherChannelId = `weather-${randomBytes(16).toString('hex')}`
console.log('Starting conversation stream...')
}
main().catch(console.error)
Step 3: Define Your Functions
Define both static and channel-based functions:
const functions = [
// Static result - returns immediately
{
name: 'get_current_time',
description: 'Get the current time for a specified timezone',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: 'The timezone, e.g. America/New_York',
},
},
required: ['timezone'],
},
result: {
data: {
time: '10:30 AM',
date: 'Monday, January 26, 2026',
},
},
},
// Channel-based result - you execute and publish
{
name: 'get_weather',
description: 'Get the current weather for a location',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'The city name',
},
},
required: ['location'],
},
result: {
channel: weatherChannelId,
},
},
]
Step 4: Start the Conversation Stream
Use complete().stream() to get a stream of events:
const stream = client.conversation
.complete(null, {
model: 'claude-4.5-sonnet',
backstory: 'You are a helpful assistant that provides weather and time information.',
messages: [
{
type: 'user',
text: 'What time is it in New York, and what is the weather like there?',
},
],
functions,
})
.stream()
Step 5: Process Stream Events
Iterate over the stream and handle different event types:
for await (const event of stream) {
switch (event.type) {
case 'token':
// Real-time token for display
process.stdout.write(event.data.token)
break
case 'waitForChannelMessageBegin':
// Function execution required
await handleChannelFunction(client, event.data)
break
case 'message':
// Complete message (bot response or activity)
handleMessage(event.data)
break
case 'result':
// Conversation completed
console.log('Conversation finished')
break
}
}
Step 6: Handle Channel Function Calls
When you receive a waitForChannelMessageBegin event, execute the function and publish the result:
async function handleChannelFunction(client, data) {
const { function: fn, channel } = data
const functionName = fn.name
const functionArgs = fn.args
console.log(`\nExecuting function: ${functionName}`)
console.log(`Arguments: ${JSON.stringify(functionArgs)}`)
// Execute your function logic
let result
if (functionName === 'get_weather') {
result = await fetchWeatherData(functionArgs.location)
} else {
result = { error: `Unknown function: ${functionName}` }
}
// Publish the result back to the channel
await client.channel.publish(channel, {
message: result,
})
console.log('Result published to channel')
}
async function fetchWeatherData(location) {
// In production, call a real weather API
return {
temperature: 42,
conditions: 'partly cloudy',
humidity: 65,
location: location,
}
}
Complete Example
Here's the complete working example:
import * as dotenv from 'dotenv'
import { ChatBotKit } from '@chatbotkit/sdk/index.js'
import { randomBytes } from 'node:crypto'
dotenv.config()
async function main() {
const client = new ChatBotKit({
secret: process.env.CHATBOTKIT_API_SECRET,
})
const weatherChannelId = `weather-${randomBytes(16).toString('hex')}`
console.log('Starting conversation stream...\n')
const stream = client.conversation
.complete(null, {
model: 'claude-4.5-sonnet',
backstory: 'You are a helpful assistant that provides weather and time information.',
messages: [
{
type: 'user',
text: 'What time is it in New York, and what is the weather like there?',
},
],
functions: [
{
name: 'get_current_time',
description: 'Get the current time for a specified timezone',
parameters: {
type: 'object',
properties: {
timezone: { type: 'string', description: 'The timezone' },
},
required: ['timezone'],
},
result: {
data: { time: '10:30 AM', date: 'Monday, January 26, 2026' },
},
},
{
name: 'get_weather',
description: 'Get the current weather for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string', description: 'The city name' },
},
required: ['location'],
},
result: { channel: weatherChannelId },
},
],
})
.stream()
for await (const event of stream) {
if (event.type === 'waitForChannelMessageBegin') {
const { function: fn, channel } = event.data
console.log(`\nExecuting: ${fn.name}`)
const result = { temperature: 42, conditions: 'partly cloudy' }
await client.channel.publish(channel, { message: result })
console.log('Result published\n')
}
if (event.type === 'message') {
const messageData = event.data
if (messageData.type === 'activity') {
const activity = messageData.meta?.activity
if (activity?.type === 'request') {
console.log(`Function call: ${activity.function?.name}`)
}
}
if (messageData.type === 'bot' && messageData.text) {
console.log(`Bot: ${messageData.text}`)
}
}
if (event.type === 'result') {
console.log('\nConversation completed')
}
}
}
main().catch(console.error)
Key Differences from Dispatch
- Direct event access: Events come directly from the stream iterator, not wrapped in a
messageenvelope - Inline execution: Your code blocks while processing the stream
- Token streaming: You can display tokens in real-time with
event.type === 'token' - Simpler event handling: Event types are directly on
event.type
Displaying Real-Time Tokens
To show tokens as they arrive (for chat-like interfaces):
for await (const event of stream) {
if (event.type === 'token') {
process.stdout.write(event.data.token)
}
// ... handle other events
}
Troubleshooting
Stream Closes Early
Ensure you handle all events before the result event. Don't break out of the loop prematurely.
Channel Timeout
If you don't publish to the channel quickly enough, the conversation may timeout. Execute functions efficiently.
Missing Results
Always check event.type carefully. The stream includes many event types - filter for what you need.