AI Layer
Provider-agnostic utilities for integrating LLMs with ChatSDK including message conversion and agent tools.
The AI module provides provider-agnostic utilities for integrating LLMs with ChatSDK. It has no dependency on any specific AI/LLM gem -- you bring your own provider.
Message Conversion
Convert ChatSDK messages into the {role, content} format expected by most LLMs:
# Fetch messages from a thread
messages, _ = adapter.fetch_messages(channel_id: "C123", thread_id: "T456")
# Convert to AI format
ai_messages = ChatSDK::AI.to_ai_messages(messages)
# => [{ role: "user", content: "Hello" }, { role: "assistant", content: "Hi there!" }]
# Include user names for multi-user conversations
ai_messages = ChatSDK::AI.to_ai_messages(messages, include_names: true)
# => [{ role: "user", content: "[Alice]: Hello" }, ...]
# Custom transform (e.g., add system context, filter)
ai_messages = ChatSDK::AI.to_ai_messages(messages) do |msg, original|
msg[:metadata] = { message_id: original.id }
msg
endSee Message Conversion for full details.
Agent Tools
Generate tool definitions for AI agents with preset permission levels:
# Create tool definitions for an agent
tools = ChatSDK::AI.create_tools(preset: :messenger)
# Three presets available:
# :reader - fetch_messages, fetch_thread (read-only)
# :messenger - reader + post_message, send_direct_message, add_reaction, start_typing
# :moderator - messenger + edit_message, delete_message, remove_reaction
# Execute a tool call from an LLM response
executor = ChatSDK::AI.create_executor(chat: chat)
result = executor.execute(:post_message, {
adapter_name: "slack",
channel_id: "C123",
text: "Hello from the AI agent!"
})See Agent Tools for full details.
Streaming LLM Responses
Bridge any Enumerable or Enumerator of string chunks to a ChatSDK streaming message:
# With any LLM client that returns an enumerable of chunks
thread.post_ai_stream(llm_chunks, placeholder: "Thinking...")
# Or use the class directly
ChatSDK::AI::StreamHandler.stream_to_thread(thread, llm_chunks)Using with RubyLLM
RubyLLM is a popular Ruby gem that provides a single interface for OpenAI, Anthropic, Google Gemini, AWS Bedrock, and 10+ other providers. It pairs naturally with ChatSDK.
Basic: synchronous response
require "ruby_llm"
RubyLLM.configure do |config|
config.openai_api_key = ENV["OPENAI_API_KEY"]
end
bot.on_new_mention do |thread, message|
# Convert chat history to LLM format
history = ChatSDK::AI.to_ai_messages(thread.messages, include_names: true)
# Chat with the LLM
chat = RubyLLM.chat(model: "gpt-4.1")
history.each { |msg| chat.add_message(role: msg[:role], content: msg[:content]) }
response = chat.complete
thread.post(response.content)
endStreaming: progressive message updates
bot.on_new_mention do |thread, message|
history = ChatSDK::AI.to_ai_messages(thread.messages)
chat = RubyLLM.chat(model: "claude-sonnet-4-20250514")
# Stream chunks directly to the chat thread
chunks = Enumerator.new do |y|
chat.stream(message.text) { |chunk| y << chunk.content if chunk.content }
end
thread.post_ai_stream(chunks, placeholder: "Thinking...")
endThe bot posts "Thinking..." immediately, then progressively edits the message as tokens arrive from the LLM.
With Anthropic gem directly
require "anthropic"
client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"])
bot.on_new_mention do |thread, message|
history = ChatSDK::AI.to_ai_messages(thread.messages)
# Stream via Enumerator
chunks = Enumerator.new do |y|
client.messages.create(
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: history,
stream: proc { |event|
y << event.dig("delta", "text") if event["type"] == "content_block_delta"
}
)
end
thread.post_ai_stream(chunks, placeholder: "Thinking...")
endWith OpenAI gem directly
require "openai"
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
bot.on_new_mention do |thread, message|
history = ChatSDK::AI.to_ai_messages(thread.messages)
chunks = Enumerator.new do |y|
client.chat(
parameters: {
model: "gpt-4.1",
messages: history,
stream: proc { |chunk|
content = chunk.dig("choices", 0, "delta", "content")
y << content if content
}
}
)
end
thread.post_ai_stream(chunks, placeholder: "Thinking...")
endKey pattern
All integrations follow the same 3-step pattern:
- Convert history:
ChatSDK::AI.to_ai_messages(thread.messages)→ standard{role, content}format - Call your LLM: any gem, any provider — wrap streaming in an
Enumeratorthat yields string chunks - Stream to chat:
thread.post_ai_stream(chunks)— handles throttled progressive edits automatically
ChatSDK doesn't care which LLM you use. It just needs an Enumerable of strings.
Provider-Agnostic Example
bot.on_new_mention do |thread, message|
# 1. Fetch conversation history
messages, _ = thread.messages
# 2. Convert to AI format
ai_messages = ChatSDK::AI.to_ai_messages(messages, include_names: true)
# 3. Call your LLM (any provider)
response = your_llm_client.chat(messages: ai_messages)
# 4. Post the response
thread.post(response.text)
end