Soka

Ruby AI Agent Framework based on ReAct Pattern

FeaturesInstallationQuick StartAdvanced FeaturesAPI DocumentationExamplesContributing

Soka is a Ruby AI Agent framework based on the ReAct (Reasoning and Acting) pattern, supporting multiple AI providers, offering an object-oriented tool system and intelligent memory management. It enables you to quickly build intelligent agents that handle complex reasoning and action tasks.

Features

  • 🤖 Multi AI Provider Support: Google Gemini, OpenAI, Anthropic
  • 🛠️ Object-Oriented Tool System: Grape API-like parameter definition and validation
  • 🧠 Intelligent Memory Management: Conversation history and thought process recording
  • 🔄 ReAct Reasoning Pattern: Tagged thought-action-observation loop
  • Flexible Configuration System: Global and instance-level configuration options
  • 🔁 Error Handling and Retry: Built-in exponential backoff retry mechanism
  • 🧪 Test Friendly: Complete test helper tools
  • 📝 Full Type Support: Using dry-rb ecosystem
  • 🚀 Modular Design: Easy to extend and maintain
  • 💾 Built-in Caching Mechanism: Improve performance and save costs

Installation

Add the following to your Gemfile:

gem 'soka'

Then execute:

bundle install

Or install directly:

gem install soka

Quick Start

1. Set up API Key

# Method 1: Environment variable
export GEMINI_API_KEY="your-api-key"

# Method 2: Create .env file
echo "GEMINI_API_KEY=your-api-key" > .env

Get API Keys:

2. Basic Usage

require 'soka'

# Create a simple time tool
class TimeTool < Soka::AgentTool
  desc "Get current time"

  def call
    Time.now.strftime('%Y-%m-%d %H:%M:%S')
  end
end

# Create Agent
class SimpleAgent < Soka::Agent
  tool TimeTool
end

# Execute
agent = SimpleAgent.new
result = agent.run("What time is it?")
puts result.final_answer

3. Run Examples

# Run full example (API key required)
ruby examples/1_basic.rb

Rails Integration

Soka Rails

For Rails applications, we provide a dedicated gem soka-rails that offers seamless integration with Rails conventions:

# Gemfile
gem 'soka-rails'

Features

  • 🚂 Native Rails Integration: Following Rails conventions and best practices
  • 📁 Auto-loading Support: Automatically loads the app/soka directory
  • 🛠️ Generator Support: Quickly generate Agent and Tool templates
  • ⚙️ Rails Configuration Integration: Uses Rails' configuration system
  • 🧪 Rails Testing Integration: Seamless integration with RSpec
  • 🔄 Rails Lifecycle Hooks: Integrates with Rails logging and error tracking

Quick Setup

# Install the gem
bundle add soka-rails

# Run the installation generator
rails generate soka:install

# Generate an agent
rails generate soka:agent customer_support

# Generate a tool
rails generate soka:tool order_lookup order_id:string

Basic Usage in Rails

# app/soka/agents/customer_support_agent.rb
class CustomerSupportAgent < ApplicationAgent
  tool OrderLookupTool
  tool UserInfoTool
end

# app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController
  def create
    agent = CustomerSupportAgent.new
    result = agent.run(params[:message])

    render json: {
      answer: result.final_answer,
      status: result.status
    }
  end
end

For more details, visit the soka-rails repository.

Core Concepts

Global Configuration

Soka.setup do |config|
  # AI Configuration
  config.ai do |ai|
    ai.provider = :gemini  # :gemini, :openai, :anthropic
    ai.model = 'gemini-2.5-flash-lite'
    ai.api_key = ENV['GEMINI_API_KEY']
  end

  # Performance Configuration
  config.performance do |perf|
    perf.max_iterations = 10      # ReAct max iterations
  end

  # Default tools
  config.tools = [SearchTool, TimeTool]
end

Defining Tools

Tools are functional modules that Agents can use:

class SearchTool < Soka::AgentTool
  desc "Search the web for information"

  params do
    requires :query, String, desc: "The query to search for"
    optional :location, String, desc: "Location context", default: "Global"

    # Parameter validation
    validates :query, presence: true, length: { minimum: 1, maximum: 500 }
    validates :location, inclusion: { in: %w[Global US Europe Asia] }, allow_nil: true
  end

  def call(query:, location: "Global")
    # Actual search logic
    perform_search(query, location)
  rescue => e
    { error: e.message, tool: self.class.name }
  end

  private

  def perform_search(query, location)
    # Here you can call real search APIs
    "Search results for #{query} in #{location}..."
  end
end

Defining Agents

Agents are the entities that perform ReAct reasoning:

class WeatherAgent < Soka::Agent
  # AI settings (override global settings)
  provider :gemini
  model 'gemini-2.5-flash-lite'
  max_iterations 10

  # Register tools
  tool SearchTool
  tool TimeTool

  # Conditional tool registration
  tool CalculatorTool, if: -> { ENV['ENABLE_CALCULATOR'] == 'true' }

  # Batch registration
  tools SearchTool, TimeTool, WeatherTool

  # Custom tool (functional) - requires description as second parameter
  tool :get_weather, "Get weather for a location"

  # Custom instructions (optional)
  instructions "You are a weather expert. Provide detailed weather information."

  # Thinking language (optional)
  think_in 'en'  # Default is 'en'

  # Lifecycle hooks
  before_action :track_action
  after_action :update_metrics
  on_error :handle_error

  private

  # Method implementation for functional tool
  # Note: This is currently experimental and not fully implemented
  def get_weather(location:)
    "#{location} is currently sunny, temperature 25°C"
  end

  def track_action(action)
    # Track action execution
    @action_count ||= 0
    @action_count += 1
  end

  def update_metrics(result)
    # Update metrics
    # metrics.record(result)
  end

  def handle_error(error, context)
    # Handle errors
    :continue  # or :stop to interrupt execution
  end
end

Using Agents

Block Mode (Real-time Feedback)

Suitable for scenarios that need to display the execution process:

agent = WeatherAgent.new

agent.run('What is the weather in Tokyo today?') do |event|
  case event.type
  when :thought
    puts "💭 Thinking: #{event.content}"
  when :action
    puts "🔧 Action: Using tool #{event.content[:tool]}"
  when :observation
    puts "👀 Observation: #{event.content}"
  when :final_answer
    puts "✅ Answer: #{event.content}"
  when :error
    puts "❌ Error: #{event.content}"
  end
end

Direct Mode (Get Result)

Suitable for scenarios that only need the final result:

agent = WeatherAgent.new
result = agent.run('What is the weather in Tokyo today?')

# Result object provides rich information
puts result.final_answer      # Final answer
puts result.iterations       # Number of iterations used
puts result.status          # :success, :failed, :max_iterations_reached
puts result.execution_time  # Execution time (if recorded)

# Check execution status
if result.successful?
  puts "Success: #{result.final_answer}"
elsif result.failed?
  puts "Failed: #{result.error}"
elsif result.max_iterations_reached?
  puts "Max iterations reached"
end

Memory Management

Basic Conversation Memory

# Initialize Agent with history
memory = [
  { role: 'user', content: 'My name is John' },
  { role: 'assistant', content: 'Hello John! Nice to meet you.' }
]

agent = WeatherAgent.new(memory: memory)
result = agent.run('What is my name?')
# => "Your name is John."

# Memory updates automatically
puts agent.memory
# <Soka::Memory> [
#   { role: 'user', content: 'My name is John' },
#   { role: 'assistant', content: 'Hello John! Nice to meet you.' },
#   { role: 'user', content: 'What is my name?' },
#   { role: 'assistant', content: 'Your name is John.' }
# ]

Thought Process Memory

# View complete thought process
puts agent.thoughts_memory
# <Soka::ThoughtsMemory> (3 sessions, 2 successful, 1 failed, avg iterations: 2.3)

# Get detailed information for specific session
last_session = agent.thoughts_memory.last_session
puts last_session[:thoughts]  # All thinking steps
puts last_session[:final_answer]  # Final answer for that execution

Advanced Features

Custom Instructions

Customize your agent's personality and response style:

class FriendlyAgent < Soka::Agent
  provider :gemini

  # Method 1: Static string instructions
  instructions "    You are a friendly, helpful assistant.\n    Use casual language and be encouraging.\n    Add emojis when appropriate.\n  PROMPT\nend\n\n# Method 2: Dynamic instructions using method\nclass DynamicAgent < Soka::Agent\n  provider :gemini\n\n  # Reference a method for dynamic instructions\n  instructions :generate_instructions\n\n  private\n\n  def generate_instructions\n    hour = Time.now.hour\n    mood = hour < 12 ? 'cheerful morning' : 'relaxed afternoon'\n\n    <<~PROMPT\n      You are a \#{mood} assistant.\n      Current Time: \#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n      Adjust your tone based on the time of day.\n    PROMPT\n  end\nend\n\n# Method 3: Override at runtime\nagent = FriendlyAgent.new(\n  instructions: 'Be more formal and professional.'\n)\nresult = agent.run(\"Help me with this task\")\n"

Multilingual Thinking (Think In)

Optimize reasoning for specific languages:

class GlobalAgent < Soka::Agent
  provider :gemini

  # Set default thinking language
  think_in 'zh-TW'  # Think in Traditional Chinese
end

# Or set dynamically
agent = GlobalAgent.new(think_in: 'ja-JP')
result = agent.run("幫我解決這個問題")  # Input in Chinese
# Agent thinks in Japanese internally, responds in Chinese

Key Points:

  • Thinking language affects internal reasoning only
  • Responses adapt to user's input language
  • Default is English ('en')
  • No automatic language detection (explicit setting required)
  • Improves reasoning quality for language-specific contexts

ReAct Flow Format

Soka uses a tagged ReAct format:

<Thought>I need to search for weather information in Tokyo</Thought>
<Action>
{"tool": "search", "parameters": {"query": "Tokyo weather", "location": "Japan"}}
</Action>
<Observation>Tokyo today: Sunny, temperature 28°C, humidity 65%</Observation>
<Thought>I have obtained the weather information and can answer the user now</Thought>
<FinalAnswer>Today in Tokyo it's sunny with a temperature of 28°C and humidity of 65%.</FinalAnswer>

Result Object Structure

# Result object attributes
result.input            # User input
result.thoughts         # Array of thinking steps
result.final_answer     # Final answer
result.status          # Status (:success, :failed, :max_iterations_reached)
result.error           # Error message (if any)
result.execution_time  # Execution time (seconds)
result.iterations      # Number of iterations

# Complete structure
{
  input: "User input",
  thoughts: [
    {
      step: 1,
      thought: "Thinking content",
      action: { tool: "search", params: { query: "..." } },
      observation: "Observation result"
    }
  ],
  final_answer: "Final answer",
  status: :success,        # :success, :failed, :max_iterations_reached
  error: nil,             # Error message (if any)
  execution_time: 1.23,   # Execution time (seconds)
  iterations: 2,          # Number of iterations
  created_at: Time        # Creation time
}

Test Support

Soka provides complete test helper tools:

RSpec.describe WeatherAgent do
  include Soka::TestHelpers

  it "answers weather questions" do
    # Mock AI response
    mock_ai_response({
      thoughts: [
        {
          step: 1,
          thought: "Need to search for weather information",
          action: { tool: "search", params: { query: "Tokyo weather" } },
          observation: "Tokyo is sunny today"
        }
      ],
      final_answer: "Tokyo is sunny today."
    })

    # Mock tool response
    mock_tool_response(SearchTool, "Tokyo is sunny today")

    agent = described_class.new
    result = agent.run("What's the weather in Tokyo?")

    expect(result).to be_successful
    expect(result.final_answer).to include("sunny")
    expect(result).to have_thoughts_count(1)
    expect(result.status).to eq(:success)
  end

  it "handles tool errors gracefully" do
    allow_tool_to_fail(SearchTool, StandardError.new("API error"))

    agent = described_class.new
    result = agent.run("Search test")

    expect(result).to be_failed
    expect(result.error).to include("API error")
  end
end

Custom Engines

You can implement your own reasoning engine:

class CustomEngine < Soka::Engines::Base
  def reason(task, &block)
    # Implement custom reasoning logic
    context = Soka::Engines::ReasoningContext.new(
      task: task,
      event_handler: block,
      max_iterations: max_iterations
    )

    # Use emit_event to send events
    emit_event(:thought, "Starting reasoning...", &block)

    # Perform reasoning...

    # Return result (using Struct)
    Soka::Engines::React::ReasonResult.new(
      input: task,
      thoughts: thoughts,
      final_answer: answer,
      status: :success
    )
  end
end

# Use custom engine
agent = MyAgent.new(engine: CustomEngine)

Examples

The examples/ directory contains several examples demonstrating different features of Soka, ordered from basic to advanced:

1. Basic Example (examples/1_basic.rb)

Demonstrates the fundamental usage of Soka with simple tools:

  • Creating basic tools (SearchTool, TimeTool)
  • Setting up an agent
  • Running queries with event handling
  • Direct result access

2. Event Handling (examples/2_event_handling.rb)

Shows how to handle real-time events during agent execution:

  • Event-based response handling
  • Different event types (thought, action, observation, final_answer)
  • Multi-step task processing
  • Direct result mode vs event mode

3. Memory Management (examples/3_memory.rb)

Illustrates memory features and conversation context:

  • Using Soka::Memory for conversation history
  • Array format for initial memory
  • Tool-based memory storage and recall
  • Accessing complete conversation history
  • Viewing thinking processes

4. Lifecycle Hooks (examples/4_hooks.rb)

Demonstrates lifecycle hooks for monitoring and control:

  • before_action for pre-processing
  • after_action for post-processing
  • on_error for error handling
  • Tracking agent activity and metrics

5. Error Handling (examples/5_error_handling.rb)

Shows robust error handling mechanisms:

  • Tool errors and agent-level errors
  • Using on_error hooks
  • Continuing execution after errors
  • Error result inspection

6. Retry Mechanisms (examples/6_retry.rb)

Demonstrates retry strategies for reliability:

  • Handling transient failures
  • Exponential backoff
  • Rate limiting scenarios
  • Configuring retry behavior

7. Conditional Tools (examples/7_tool_conditional.rb)

Shows dynamic tool loading based on conditions:

  • Environment-based tool loading
  • Role-based access control
  • Feature flag integration
  • Time-based availability

8. Multi-Provider Support (examples/8_multi_provider.rb)

Demonstrates using different AI providers:

  • Configuring Gemini, OpenAI, and Anthropic
  • Provider-specific features
  • Comparing outputs across models
  • Cost optimization strategies

9. Custom Instructions (examples/9_custom_instructions.rb)

Shows how to customize agent personality:

  • Setting instructions at class level
  • Runtime instruction override
  • Creating different agent personalities
  • Use cases for different domains

10. Dynamic Instructions (examples/10_dynamic_instructions.rb)

Demonstrates dynamic instruction generation:

  • Using methods to generate instructions dynamically
  • Time-based instruction changes
  • Environment-based configuration
  • Session-based personality switching

11. Multilingual Thinking (examples/11_think_in_languages.rb)

Demonstrates language-specific reasoning:

  • Setting thinking language with think_in
  • Class-level vs instance-level configuration
  • Performance comparison across languages
  • Cultural context optimization

To run any example:

# Make sure you have the required API keys in your .env file
ruby examples/1_basic.rb

API Documentation

Supported AI Providers

Google Gemini

  • Models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite
  • Environment variable: GEMINI_API_KEY
  • Features: Fast response, cost-effective
  • Default model: gemini-2.5-flash-lite

OpenAI

  • Models: gpt-5, gpt-5-mini, gpt-5-nano
  • Environment variable: OPENAI_API_KEY
  • Features: Streaming support, powerful reasoning

Anthropic

  • Models: claude-opus-4-0, claude-sonnet-4-0, claude-3-5-haiku-latest
  • Environment variable: ANTHROPIC_API_KEY
  • Features: Long context support, excellent code understanding

Model Testing Results

Based on extensive testing with the Soka framework, here are the recommended models for optimal performance:

Provider ✅ Recommended Models ⚠️ Not Recommended Notes
Gemini gemini-2.5-flash-lite
gemini-2.5-pro
gemini-2.5-flash gemini-2.5-flash-lite: Fast, cost-effective, with good performance
Avoid gemini-2.5-flash: Often skips thinking process when using tools, directly jumps to tool usage
OpenAI gpt-5
gpt-5-mini
gpt-5-nano gpt-5-mini: Good balance of price, speed, and effectiveness
Avoid gpt-5-nano: Can enter infinite tool-calling loops, fails to complete tasks
Claude To be tested - Testing in progress

⭐ = Best choice for most use cases

Testing Insights

  • Thinking Process: Models marked as "Not Recommended" often fail to properly follow the ReAct pattern, either skipping the thinking phase or getting stuck in loops
  • Cost-Performance: gemini-2.5-flash-lite and gpt-5-mini offer the best balance for most applications
  • Reliability: Recommended models consistently complete tasks without entering error states

Configuration Options

Option Type Default Description
ai.provider Symbol :gemini AI provider
ai.model String "gemini-2.5-flash-lite" Model to use
ai.api_key String nil API key
ai.instructions String nil Custom agent instructions
ai.think_in String "en" Thinking language
performance.max_iterations Integer 10 Max iterations

Tool Parameter Validation

Validator Options Description
presence true/false Value cannot be empty
length minimum, maximum String length limits
inclusion in, allow_nil Value must be in specified list
format with Match regular expression

Performance Optimization

  1. Use appropriate models:

    • Simple tasks: gemini-2.5-flash-lite or gpt-5-mini or claude-3-5-haiku-latest
    • Complex reasoning: gemini-2.5-pro or gpt-5 or claude-sonnet-4-0
  2. Control iterations:

    agent = MyAgent.new(max_iterations: 5)  # Limit iterations
    

Troubleshooting

Common Issues

  1. API Key Error

    Soka::LLMError: API key is required
    

    Solution: Ensure correct environment variable is set or provide API key in configuration

  2. Max Iterations Reached

    Status: max_iterations_reached
    

    Solution: Simplify the problem or increase max_iterations

Debugging Tips

# Adjust max iterations
Soka.configure do |c|
  c.performance.max_iterations = 20
end

# Use block mode to see execution process
agent.run(query) do |event|
  p event  # Print all events
end

# Inspect thought process
result = agent.run(query)
result.thoughts.each do |thought|
  puts "Step #{thought[:step]}: #{thought[:thought]}"
  puts "Action: #{thought[:action]}" if thought[:action]
  puts "Observation: #{thought[:observation]}" if thought[:observation]
end

Development

# Install dependencies
bundle install

# Run tests
bundle exec rspec

# Run Rubocop
bundle exec rubocop

# Open interactive console
bin/console

# Create new version
# 1. Update lib/soka/version.rb
# 2. Update CHANGELOG.md
# 3. Commit changes
# 4. Create tag
bundle exec rake release

Contributing

We welcome all forms of contributions!

  1. Fork the project
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Please ensure:

  • Add appropriate tests
  • Update relevant documentation
  • Follow existing code style
  • Pass Rubocop checks

License

This project is licensed under the MIT License. See the LICENSE file for details.

Acknowledgments

  • Thanks to the ReAct paper for the theoretical foundation
  • Thanks to the Regent project for architectural inspiration
  • Thanks to all contributors for their efforts

Made with ❤️ in Taiwan
Created by Claude Code