💎 ChatSDK Ruby

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
end

See 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)
end

Streaming: 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...")
end

The 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...")
end

With 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...")
end

Key pattern

All integrations follow the same 3-step pattern:

  1. Convert history: ChatSDK::AI.to_ai_messages(thread.messages) → standard {role, content} format
  2. Call your LLM: any gem, any provider — wrap streaming in an Enumerator that yields string chunks
  3. 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

On this page