Ruby SDK

Official Ruby SDK for Kai the AI

Published

November 20, 2025

Overview

The official Ruby SDK for Kai the AI provides an elegant, idiomatic Ruby interface for integrating intelligent teaching assistance into your Ruby and Rails applications. Built with Ruby 2.7+ features, the SDK embraces Ruby conventions with blocks, symbols, and expressive DSLs while supporting both synchronous and asynchronous operations.

Installation

Add the SDK to your project using Bundler:

# Gemfile
gem 'kai-sdk', '~> 1.0'

Then install:

bundle install
gem install kai-sdk

For Rails applications, add to your Gemfile and create an initializer:

# Gemfile
gem 'kai-sdk', '~> 1.0'

# config/initializers/kai.rb
Kai.configure do |config|
  config.api_key = ENV['KAI_API_KEY']
  config.timeout = 30
  config.max_retries = 3
end

Requirements

  • Ruby 2.7 or higher
  • Active Kai the AI API key
  • Internet connection for API requests

Quick Start

Here’s a minimal example to get you started:

require 'kai-sdk'

# Initialize the client
client = Kai::Client.new( 'your_api_key_here')

# Grade a student submission
result = client.assignments.grade(
   'CS101-HW1',
   'The sorting algorithm works by...',
   'detailed'
)

puts "Score: #{result.score}/100"
puts "Feedback: #{result.feedback}"

Authentication

API Key Setup

The SDK requires an API key for authentication. You can provide the key in several ways:

export KAI_API_KEY="your_api_key_here"
require 'kai-sdk'

# Automatically reads from KAI_API_KEY environment variable
client = Kai::Client.new
require 'kai-sdk'

client = Kai::Client.new( 'your_api_key_here')
require 'kai-sdk'

Kai.configure do |config|
  config.api_key = 'your_api_key_here'
  config.base_url = 'https://chi2api.com/v1'
  config.timeout = 30
end

client = Kai::Client.new
# config/initializers/kai.rb
Kai.configure do |config|
  config.api_key = Rails.application.credentials.dig(, )
end
WarningSecurity Best Practice

Never hardcode API keys in your source code or commit them to version control. Use environment variables, Rails credentials, or secure secret management systems.

Core Classes

Kai::Client

The main client class for interacting with Kai’s API.

module Kai
  class Client
    # Create a new client instance
    #
    # @param api_key [String] Your API key (defaults to ENV['KAI_API_KEY'])
    # @param base_url [String] API base URL
    # @param timeout [Integer] Request timeout in seconds
    # @param max_retries [Integer] Maximum retry attempts
    def initialize(api_key: nil,  nil,  30,  3)
      # ...
    end

    # Access assignment grading operations
    # @return [Kai::Assignments]
    def assignments
      @assignments ||= Kai::Assignments.new(self)
    end

    # Access content generation operations
    # @return [Kai::Content]
    def content
      @content ||= Kai::Content.new(self)
    end

    # Access student analytics operations
    # @return [Kai::Analytics]
    def analytics
      @analytics ||= Kai::Analytics.new(self)
    end

    # Access quiz generation operations
    # @return [Kai::Quizzes]
    def quizzes
      @quizzes ||= Kai::Quizzes.new(self)
    end
  end
end

Kai::Assignments

Handle assignment grading and feedback operations.

module Kai
  class Assignments
    # Grade a student submission
    #
    # @param assignment_id [String] Assignment identifier
    # @param student_submission [String] Student's work
    # @param rubric_id [String, nil] Optional rubric to apply
    # @param grading_style [String] "detailed", "concise", or "points_only"
    # @return [Kai::GradeResult]
    # @raise [Kai::ValidationError] Invalid parameters
    # @raise [Kai::RateLimitError] Rate limit exceeded
    # @raise [Kai::APIError] API request failed
    def grade(assignment_id:, ,  nil,  'detailed')
      # ...
    end

    # Grade with a block for additional configuration
    #
    # @yield [request] Configuration block
    # @return [Kai::GradeResult]
    def grade_with(&block)
      request = GradeRequest.new
      block.call(request)
      execute_grade(request)
    end

    # Retrieve a previously generated grade
    #
    # @param grade_id [String] Grade identifier
    # @return [Kai::GradeResult]
    def get_grade(grade_id)
      # ...
    end

    # Grade multiple submissions in batch
    #
    # @param submissions [Array<Hash>] Array of submission data
    # @return [Array<Kai::GradeResult>]
    def batch_grade(submissions)
      # ...
    end
  end
end

Kai::Quizzes

Generate and manage quiz content.

module Kai
  class Quizzes
    # Generate quiz questions
    #
    # @param topic [String] Subject matter for quiz
    # @param difficulty [String] "easy", "medium", or "hard"
    # @param question_count [Integer] Number of questions (1-50)
    # @param question_types [Array<String>] Types of questions to include
    # @return [Kai::QuizResult]
    def generate(topic:,  'medium',  10,  nil)
      # ...
    end

    # Generate quiz with configuration block
    #
    # @yield [request] Configuration block
    # @return [Kai::QuizResult]
    def generate_with(&block)
      request = QuizRequest.new
      block.call(request)
      execute_generate(request)
    end

    # Retrieve a previously generated quiz
    #
    # @param quiz_id [String] Quiz identifier
    # @return [Kai::QuizResult]
    def get_quiz(quiz_id)
      # ...
    end
  end
end

Kai::Analytics

Access student performance data and analytics.

module Kai
  class Analytics
    # Retrieve student performance metrics
    #
    # @param student_id [String] Student identifier
    # @param start_date [String, Date, Time] Start date filter (ISO 8601)
    # @param end_date [String, Date, Time] End date filter (ISO 8601)
    # @param course_id [String, nil] Optional course filter
    # @return [Kai::PerformanceData]
    def student_performance(student_id:,  nil,  nil,  nil)
      # ...
    end

    # Retrieve class-level analytics
    #
    # @param course_id [String] Course identifier
    # @param metric_types [Array<String>, nil] Specific metrics to retrieve
    # @return [Kai::ClassAnalytics]
    def class_analytics(course_id:,  nil)
      # ...
    end
  end
end

Response Models

Kai::GradeResult

module Kai
  class GradeResult
    attr_reader , , , ,
                , 

    # @param attributes [Hash] Grade result attributes
    def initialize(attributes = {})
      @grade_id = attributes[]
      @score = attributes[]
      @feedback = attributes[]
      @suggestions = attributes[] || []
      @timestamp = parse_time(attributes[])
      @rubric_breakdown = attributes[]
    end

    # Check if grade meets passing threshold (>= 70)
    # @return [Boolean]
    def passed?
      score >= 70
    end

    # Get letter grade based on score
    # @return [String]
    def letter_grade
      case score
      when 90..100 then 'A'
      when 80..89 then 'B'
      when 70..79 then 'C'
      when 60..69 then 'D'
      else 'F'
      end
    end

    def to_h
      {
         grade_id,
         score,
         feedback,
         suggestions,
         timestamp,
         rubric_breakdown
      }
    end
  end
end

Kai::QuizResult

module Kai
  class QuizQuestion
    attr_reader , , ,
                , , 

    def initialize(attributes = {})
      @question_id = attributes[]
      @question_text = attributes[]
      @question_type = attributes[]
      @options = attributes[]
      @correct_answer = attributes[]
      @explanation = attributes[]
    end

    def multiple_choice?
      question_type == 'multiple_choice'
    end

    def true_false?
      question_type == 'true_false'
    end
  end

  class QuizResult
    attr_reader , , , ,
                

    def initialize(attributes = {})
      @quiz_id = attributes[]
      @topic = attributes[]
      @difficulty = attributes[]
      @questions = parse_questions(attributes[])
      @estimated_time_minutes = attributes[]
    end

    # Get total number of questions
    # @return [Integer]
    def question_count
      questions.size
    end

    # Iterate over questions
    # @yield [question] Each question
    def each_question(&block)
      questions.each(&block)
    end

    private

    def parse_questions(questions_data)
      (questions_data || []).map { |q| QuizQuestion.new(q) }
    end
  end
end

Kai::PerformanceData

module Kai
  class PerformanceData
    attr_reader , , ,
                , , ,
                , 

    def initialize(attributes = {})
      @student_id = attributes[]
      @average_score = attributes[].to_f
      @assignments_completed = attributes[]
      @assignments_total = attributes[]
      @strength_areas = attributes[] || []
      @improvement_areas = attributes[] || []
      @trend = attributes[]
      @last_updated = parse_time(attributes[])
    end

    # Calculate completion rate as percentage
    # @return [Float] Completion rate (0-100)
    def completion_rate
      return 0.0 if assignments_total.zero?
      (assignments_completed.to_f / assignments_total * 100).round(1)
    end

    # Check if student is improving
    # @return [Boolean]
    def improving?
      trend == 'improving'
    end

    # Check if student is declining
    # @return [Boolean]
    def declining?
      trend == 'declining'
    end
  end
end

Complete API Methods

Assignment Grading

require 'kai-sdk'

client = Kai::Client.new

# Grade a single submission
result = client.assignments.grade(
   'CS101-HW1',
   'My algorithm implementation...',
   'standard_coding_rubric',
   'detailed'
)

puts "Grade ID: #{result.grade_id}"
puts "Score: #{result.score}/100"
puts "Letter Grade: #{result.letter_grade}"
puts "Feedback: #{result.feedback}"

if result.rubric_breakdown
  puts "\nRubric Breakdown:"
  result.rubric_breakdown.each do |criterion, points|
    puts "  #{criterion}: #{points}"
  end
end

puts "\nSuggestions:"
result.suggestions.each do |suggestion|
  puts "  - #{suggestion}"
end
require 'kai-sdk'

client = Kai::Client.new

# Use block for cleaner configuration
result = client.assignments.grade_with do |req|
  req.assignment_id = 'CS101-HW1'
  req.student_submission = 'My algorithm implementation...'
  req.rubric_id = 'standard_coding_rubric'
  req.grading_style = 'detailed'
end

puts "Score: #{result.score}/100"
puts "Passed: #{result.passed? ? 'Yes' : 'No'}"
require 'kai-sdk'

client = Kai::Client.new

begin
  result = client.assignments.grade(
     'CS101-HW1',
     'Student work...',
     'detailed'
  )

  puts "Score: #{result.score}/100"

rescue Kai: => e
  puts "Invalid parameters: #{e.message}"
  puts "Details: #{e.details}"

rescue Kai: => e
  puts "Authentication failed: #{e.message}"
  puts "Please check your API key"

rescue Kai: => e
  puts "Rate limit exceeded: #{e.message}"
  puts "Retry after: #{e.retry_after} seconds"

rescue Kai: => e
  puts "Request timed out: #{e.message}"

rescue Kai: => e
  puts "API error: #{e.message}"
  puts "Status code: #{e.status_code}"

rescue Kai: => e
  puts "SDK error: #{e.message}"
end

Batch Grading

Grade multiple submissions efficiently:

require 'kai-sdk'

client = Kai::Client.new

submissions = [
  {
     'CS101-HW1',
     'student_001',
     "First student's work..."
  },
  {
     'CS101-HW1',
     'student_002',
     "Second student's work..."
  },
  {
     'CS101-HW1',
     'student_003',
     "Third student's work..."
  }
]

results = client.assignments.batch_grade(submissions)

results.each do |result|
  puts "Student: Score #{result.score}/100"
end

# Calculate class statistics
average_score = results.sum(&) / results.size.to_f
puts "Class average: #{average_score.round(1)}/100"

# Filter passing students
passing = results.select(&)
puts "Passing students: #{passing.size}/#{results.size}"

Quiz Generation

require 'kai-sdk'

client = Kai::Client.new

quiz = client.quizzes.generate(
   'Ruby Fundamentals',
   'medium',
   10,
   ['multiple_choice', 'true_false']
)

puts "Quiz ID: #{quiz.quiz_id}"
puts "Topic: #{quiz.topic}"
puts "Estimated time: #{quiz.estimated_time_minutes} minutes"
puts "Questions: #{quiz.question_count}"

quiz.each_question.with_index(1) do |question, i|
  puts "\nQuestion #{i}: #{question.question_text}"

  if question.options
    question.options.each do |opt|
      puts "  #{opt}"
    end
  end
end
require 'kai-sdk'

client = Kai::Client.new

quiz = client.quizzes.generate_with do |req|
  req.topic = 'Ruby on Rails'
  req.difficulty = 'hard'
  req.question_count = 15
  req.question_types = ['multiple_choice', 'short_answer']
end

# Export quiz to hash
quiz_data = {
   quiz.quiz_id,
   quiz.topic,
   quiz.difficulty,
   quiz.estimated_time_minutes,
   quiz.questions.map(&)
}

# Save as JSON
File.write('quiz.json', JSON.pretty_generate(quiz_data))
puts "Quiz saved to quiz.json"
require 'kai-sdk'

client = Kai::Client.new

topics = [
  {  'Ruby Basics',  'easy',  15 },
  {  'Object-Oriented Programming',  'medium',  20 },
  {  'Metaprogramming',  'hard',  10 }
]

quizzes = topics.map do |config|
  client.quizzes.generate(
     config[],
     config[],
     config[]
  )
end

quizzes.each do |quiz|
  puts "Generated: #{quiz.topic} (#{quiz.question_count} questions)"
end

Student Analytics

require 'kai-sdk'
require 'date'

client = Kai::Client.new

# Get performance for the last 30 days
end_date = Date.today
start_date = end_date - 30

performance = client.analytics.student_performance(
   'student_12345',
   start_date.iso8601,
   end_date.iso8601,
   'CS101'
)

puts "Average Score: #{performance.average_score.round(1)}"
puts "Completion Rate: #{performance.assignments_completed}/#{performance.assignments_total} (#{performance.completion_rate}%)"
puts "Trend: #{performance.trend}"

puts "\nStrengths:"
performance.strength_areas.each do |area|
  puts "  ✓ #{area}"
end

puts "\nAreas for Improvement:"
performance.improvement_areas.each do |area|
  puts "  ! #{area}"
end

# Check status
if performance.declining?
  puts "\n⚠️  Student performance is declining"
elsif performance.improving?
  puts "\n✅ Student performance is improving"
end

Error Handling

The SDK provides specific exception types for different error scenarios:

require 'kai-sdk'

client = Kai::Client.new

begin
  result = client.assignments.grade(
     'CS101-HW1',
     'Student work...',
     'detailed'
  )

  puts "Score: #{result.score}"

rescue Kai: => e
  # Handle invalid parameters
  puts "Invalid input: #{e.message}"
  puts "Details: #{e.details.inspect}"

rescue Kai: => e
  # Handle authentication failures
  puts "Authentication failed: #{e.message}"
  puts "Please check your API key"

rescue Kai: => e
  # Handle rate limiting
  puts "Rate limit exceeded: #{e.message}"
  puts "Retry after: #{e.retry_after} seconds"

rescue Kai: => e
  # Handle timeouts
  puts "Request timed out: #{e.message}"
  puts "Try increasing the timeout parameter"

rescue Kai: => e
  # Handle general API errors
  puts "API error: #{e.message}"
  puts "Status code: #{e.status_code}"
  puts "Error code: #{e.error_code}"

rescue Kai: => e
  # Catch-all for SDK errors
  puts "SDK error: #{e.message}"
end

Exception Hierarchy

Kai::Error (base)
├── Kai:        # Invalid parameters
├── Kai:    # Invalid API key
├── Kai:        # Rate limit exceeded
├── Kai:          # Request timeout
└── Kai:              # General API error
TipError Recovery Pattern

Implement exponential backoff for rate limit errors:

def grade_with_retry(client, assignment_id, submission, max_retries: 3)
  retry_delay = 1

  max_retries.times do |attempt|
    begin
      return client.assignments.grade(
         assignment_id,
         submission
      )
    rescue Kai: => e
      if attempt < max_retries - 1
        wait_time = retry_delay * (2 ** attempt)
        puts "Rate limited, waiting #{wait_time}s..."
        sleep(wait_time)
      else
        raise
      end
    end
  end
end

Rails Integration

Configuration

# config/initializers/kai.rb
Kai.configure do |config|
  config.api_key = Rails.application.credentials.dig(, )
  config.timeout = 30
  config.max_retries = 3
  config.logger = Rails.logger
end

Model Integration

# app/models/assignment.rb
class Assignment < ApplicationRecord
  has_many 

  def grade_submission(submission)
    client = Kai::Client.new

    result = client.assignments.grade(
       id.to_s,
       submission.content,
       rubric_id,
       'detailed'
    )

    submission.update!(
       result.score,
       result.feedback,
       Time.current
    )

    result
  rescue Kai: => e
    Rails.logger.error("Grading failed: #{e.message}")
    nil
  end
end

Service Object Pattern

# app/services/grading_service.rb
class GradingService
  def initialize(client: nil)
    @client = client || Kai:.new
  end

  def grade_submission(assignment, submission)
    result = @client.assignments.grade(
       assignment.id.to_s,
       submission.content,
       'detailed'
    )

    update_submission(submission, result)
    notify_student(submission)

    result
  end

  def batch_grade(assignment, submissions)
    submission_data = submissions.map do |sub|
      {
         assignment.id.to_s,
         sub.student_id.to_s,
         sub.content
      }
    end

    results = @client.assignments.batch_grade(submission_data)

    submissions.zip(results).each do |submission, result|
      update_submission(submission, result)
    end

    results
  end

  private

  def update_submission(submission, result)
    submission.update!(
       result.score,
       result.feedback,
       result.suggestions,
       Time.current
    )
  end

  def notify_student(submission)
    StudentMailer.grade_notification(submission).deliver_later
  end
end

Background Job

# app/jobs/grade_submission_job.rb
class GradeSubmissionJob < ApplicationJob
  queue_as 
  retry_on Kai:,  ,  5

  def perform(submission_id)
    submission = Submission.find(submission_id)
    assignment = submission.assignment

    client = Kai::Client.new

    result = client.assignments.grade(
       assignment.id.to_s,
       submission.content,
       'detailed'
    )

    submission.update!(
       result.score,
       result.feedback,
       Time.current
    )

    StudentMailer.grade_notification(submission).deliver_now

  rescue Kai: => e
    Rails.logger.error("Grading failed for submission #{submission_id}: #{e.message}")
    submission.update!( e.message)
    raise
  end
end

# Usage in controller
class SubmissionsController < ApplicationController
  def create
    @submission = current_student.submissions.build(submission_params)

    if @submission.save
      GradeSubmissionJob.perform_later(@submission.id)
      redirect_to @submission,  'Submission received. Grading in progress.'
    else
      render 
    end
  end
end

Controller Integration

# app/controllers/api/v1/grading_controller.rb
module Api
  module V1
    class GradingController < ApplicationController
      before_action 

      def create
        service = GradingService.new

        result = service.grade_submission(
          assignment,
          submission
        )

        if result
          render  {
             result.grade_id,
             result.score,
             result.feedback,
             result.suggestions
          },  
        else
          render  {  'Grading failed' },  
        end
      end

      def batch_create
        service = GradingService.new
        results = service.batch_grade(assignment, submissions)

        render  results.map { |r|
          {
             r.grade_id,
             r.score,
             r.feedback
          }
        },  
      end

      private

      def assignment
        @assignment ||= Assignment.find(params[])
      end

      def submission
        @submission ||= assignment.submissions.find(params[])
      end

      def submissions
        @submissions ||= assignment.submissions.where( params[])
      end
    end
  end
end

Complete Working Examples

Example 1: Automated Grading Pipeline

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'kai-sdk'
require 'csv'

# Automated grading pipeline for processing student submissions from CSV
class AutomatedGradingPipeline
  def initialize(client)
    @client = client
  end

  def load_submissions(csv_path)
    submissions = []

    CSV.foreach(csv_path,  true) do |row|
      submissions << {
         row['student_id'],
         row['assignment_id'],
         row['submission_text']
      }
    end

    submissions
  end

  def grade_submissions(submissions)
    results = []

    submissions.each do |submission|
      begin
        result = @client.assignments.grade(
           submission[],
           submission[],
           'detailed'
        )

        results << {
           submission[],
           result.score,
           result.feedback,
           result.suggestions.join(', ')
        }

        puts "✓ Graded #{submission[]}: #{result.score}/100"

      rescue Kai: => e
        puts "✗ Failed to grade #{submission[]}: #{e.message}"

        results << {
           submission[],
           nil,
           "Error: #{e.message}",
           ''
        }
      end
    end

    results
  end

  def save_results(results, output_path)
    CSV.open(output_path, 'w') do |csv|
      csv << %w[student_id score feedback suggestions]

      results.each do |result|
        csv << [
          result[],
          result[],
          result[],
          result[]
        ]
      end
    end

    puts "\n✓ Results saved to #{output_path}"
  end

  def print_summary(results)
    successful = results.count { |r| r[] }
    scores = results.map { |r| r[] }.compact
    avg_score = scores.sum / scores.size.to_f

    puts "\n=== Summary ==="
    puts "Total submissions: #{results.size}"
    puts "Successfully graded: #{successful}"
    puts "Average score: #{avg_score.round(1)}/100"
  end

  def run(input_path, output_path)
    submissions = load_submissions(input_path)
    puts "Loaded #{submissions.size} submissions"

    results = grade_submissions(submissions)
    save_results(results, output_path)
    print_summary(results)
  end
end

# Run the pipeline
if __FILE__ == $PROGRAM_NAME
  client = Kai::Client.new
  pipeline = AutomatedGradingPipeline.new(client)

  pipeline.run('submissions.csv', 'grading_results.csv')
end

Example 2: Quiz Generator with Export

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'kai-sdk'
require 'json'

# Generate quizzes for multiple topics and export to various formats
class QuizGenerator
  def initialize(client)
    @client = client
  end

  def generate_quiz(topic, difficulty: 'medium',  10)
    quiz = @client.quizzes.generate(
       topic,
       difficulty,
       count,
       ['multiple_choice', 'true_false', 'short_answer']
    )

    puts "\n✓ Generated quiz: #{topic}"
    puts "  ID: #{quiz.quiz_id}"
    puts "  Questions: #{quiz.question_count}"
    puts "  Time: ~#{quiz.estimated_time_minutes} minutes"

    quiz
  end

  def export_to_json(quiz, filename)
    quiz_data = {
       quiz.quiz_id,
       quiz.topic,
       quiz.difficulty,
       quiz.estimated_time_minutes,
       quiz.questions.map do |q|
        {
           q.question_id,
           q.question_text,
           q.question_type,
           q.options,
           q.correct_answer,
           q.explanation
        }
      end
    }

    File.write(filename, JSON.pretty_generate(quiz_data))
    puts "✓ Exported to #{filename}"
  end

  def export_to_markdown(quiz, filename)
    File.open(filename, 'w') do |f|
      f.puts "# #{quiz.topic} Quiz\n\n"
      f.puts "**Difficulty:** #{quiz.difficulty}\n"
      f.puts "**Estimated Time:** #{quiz.estimated_time_minutes} minutes\n\n"
      f.puts "---\n\n"

      quiz.each_question.with_index(1) do |question, i|
        f.puts "## Question #{i}\n\n"
        f.puts "#{question.question_text}\n\n"

        if question.options
          question.options.each do |opt|
            f.puts "- #{opt}\n"
          end
          f.puts "\n"
        end

        if question.explanation
          f.puts "**Explanation:** #{question.explanation}\n\n"
        end

        f.puts "---\n\n"
      end
    end

    puts "✓ Exported to #{filename}"
  end

  def generate_all(topics)
    topics.each do |config|
      quiz = generate_quiz(
        config[],
         config[],
         config[]
      )

      base_name = config[].downcase.gsub(' ', '_')
      export_to_json(quiz, "#{base_name}_quiz.json")
      export_to_markdown(quiz, "#{base_name}_quiz.md")
    end

    puts "\n✓ All quizzes generated and exported successfully!"
  end
end

# Run the generator
if __FILE__ == $PROGRAM_NAME
  client = Kai::Client.new
  generator = QuizGenerator.new(client)

  topics = [
    {  'Ruby Fundamentals',  'easy',  15 },
    {  'Rails Development',  'medium',  20 },
    {  'Ruby Metaprogramming',  'hard',  10 }
  ]

  generator.generate_all(topics)
end

Example 3: Student Analytics Dashboard

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'kai-sdk'
require 'csv'
require 'date'

# Generate comprehensive student analytics reports
class StudentAnalyticsDashboard
  def initialize(client)
    @client = client
  end

  def load_roster(csv_path)
    student_ids = []

    CSV.foreach(csv_path,  true) do |row|
      student_ids << row['student_id']
    end

    student_ids
  end

  def analyze_student(student_id, course_id)
    end_date = Date.today
    start_date = end_date - 90 # Last 90 days

    performance = @client.analytics.student_performance(
       student_id,
       start_date.iso8601,
       end_date.iso8601,
       course_id
    )

    {
       student_id,
       performance.average_score,
       performance.completion_rate,
       performance.trend,
       performance.strength_areas,
       performance.improvement_areas
    }
  end

  def analyze_class(student_ids, course_id)
    analytics_data = []

    student_ids.each do |student_id|
      begin
        data = analyze_student(student_id, course_id)
        analytics_data << data
        puts "✓ Analyzed #{student_id}"
      rescue Kai: => e
        puts "✗ Failed to analyze #{student_id}: #{e.message}"
      end
    end

    analytics_data
  end

  def generate_report(analytics_data, output_dir)
    Dir.mkdir(output_dir) unless Dir.exist?(output_dir)

    # Save detailed CSV
    csv_path = File.join(output_dir, 'class_analytics.csv')
    CSV.open(csv_path, 'w') do |csv|
      csv << %w[student_id average_score completion_rate trend strengths improvements]

      analytics_data.each do |data|
        csv << [
          data[],
          data[].round(1),
          data[].round(1),
          data[],
          data[].join('; '),
          data[].join('; ')
        ]
      end
    end

    # Generate summary report
    summary_path = File.join(output_dir, 'class_summary.txt')
    File.open(summary_path, 'w') do |f|
      write_summary(f, analytics_data)
    end

    puts "\n✓ Reports saved to #{output_dir}/"
    puts "  - #{csv_path}"
    puts "  - #{summary_path}"
  end

  def write_summary(file, analytics_data)
    scores = analytics_data.map { |d| d[] }
    completion_rates = analytics_data.map { |d| d[] }

    file.puts "CLASS ANALYTICS REPORT"
    file.puts "=" * 50
    file.puts

    file.puts "Total Students: #{analytics_data.size}"
    file.puts "Class Average: #{(scores.sum / scores.size).round(1)}"
    file.puts "Average Completion Rate: #{(completion_rates.sum / completion_rates.size).round(1)}%"
    file.puts

    # Students by trend
    improving = analytics_data.count { |d| d[] == 'improving' }
    stable = analytics_data.count { |d| d[] == 'stable' }
    declining = analytics_data.count { |d| d[] == 'declining' }

    file.puts "Student Trends:"
    file.puts "  Improving: #{improving} (#{(improving.to_f / analytics_data.size * 100).round(1)}%)"
    file.puts "  Stable: #{stable} (#{(stable.to_f / analytics_data.size * 100).round(1)}%)"
    file.puts "  Declining: #{declining} (#{(declining.to_f / analytics_data.size * 100).round(1)}%)"
    file.puts

    # Students needing attention
    file.puts "Students Needing Attention:"
    analytics_data.each do |data|
      if data[] < 70 || data[] == 'declining'
        file.puts "  - #{data[]}: Score #{data[].round(1)}, #{data[]}"
      end
    end
  end

  def run(roster_path, course_id, output_dir)
    student_ids = load_roster(roster_path)
    puts "Analyzing #{student_ids.size} students..."

    analytics_data = analyze_class(student_ids, course_id)
    generate_report(analytics_data, output_dir)

    puts "\n✓ Analytics complete!"
  end
end

# Run the dashboard
if __FILE__ == $PROGRAM_NAME
  client = Kai::Client.new
  dashboard = StudentAnalyticsDashboard.new(client)

  dashboard.run('roster.csv', 'CS101', 'analytics_report')
end

Rake Tasks

Create custom rake tasks for common operations:

# lib/tasks/kai.rake
namespace  do
  desc 'Grade all pending submissions'
  task   do
    client = Kai::Client.new

    Submission.pending.find_each do |submission|
      begin
        result = client.assignments.grade(
           submission.assignment_id.to_s,
           submission.content
        )

        submission.update!(
           result.score,
           result.feedback,
           Time.current
        )

        puts "✓ Graded submission #{submission.id}"
      rescue Kai: => e
        puts "✗ Failed to grade submission #{submission.id}: #{e.message}"
      end
    end
  end

  desc 'Generate weekly analytics report'
  task   do
    client = Kai::Client.new

    Course.find_each do |course|
      analytics = client.analytics.class_analytics( course.id.to_s)

      WeeklyReport.create!(
         course,
         analytics.to_h,
         Time.current
      )

      puts "✓ Generated report for #{course.name}"
    end
  end
end

Best Practices

NoteRuby Idioms and Best Practices
  1. Use Symbols for Keys: Follow Ruby conventions

    # Good: Using symbols
    client.assignments.grade(
       'CS101',
       'Work...'
    )
    
    # Avoid: Using strings
    client.assignments.grade(
      'assignment_id' => 'CS101',
      'student_submission' => 'Work...'
    )
  2. Leverage Blocks: Use Ruby’s block syntax for configuration

    # Good: Block-based configuration
    Kai.configure do |config|
      config.api_key = ENV['KAI_API_KEY']
      config.timeout = 30
    end
    
    # Also good: Chaining with blocks
    quiz = client.quizzes.generate_with do |req|
      req.topic = 'Ruby'
      req.difficulty = 'medium'
      req.question_count = 10
    end
  3. Use Safe Navigation: Handle nil values gracefully

    # Good: Safe navigation
    score = result&.score || 0
    feedback = result&.feedback&.truncate(100)
    
    # Use presence for nil/empty checks
    api_key = ENV['KAI_API_KEY'].presence || 'default_key'
  4. Follow Rails Patterns: Use service objects and jobs

    # Good: Service object
    class GradingService
      def call(submission)
        # Grading logic
      end
    end
    
    # Good: Background job
    GradeSubmissionJob.perform_later(submission.id)
  5. Handle Errors Idiomatically: Use rescue with specific exceptions

    # Good: Specific exception handling
    begin
      result = client.assignments.grade(...)
    rescue Kai: => e
      sleep(e.retry_after)
      retry
    rescue Kai: => e
      Rails.logger.error("Grading failed: #{e.message}")
      nil
    end
  6. Use Enumerable Methods: Leverage Ruby’s powerful enumerables

    # Good: Functional style
    passing = results.select(&)
    scores = results.map(&)
    average = scores.sum / scores.size.to_f
    
    # Chain operations
    top_performers = results
      .select { |r| r.score >= 90 }
      .sort_by(&)
      .reverse
      .first(5)

Testing

RSpec Examples

# spec/services/grading_service_spec.rb
require 'rails_helper'

RSpec.describe GradingService do
  let() { instance_double(Kai::Client) }
  let() { described_class.new( client) }

  describe '#grade_submission' do
    let() { create() }
    let() { create(,  assignment) }

    let() do
      Kai::GradeResult.new(
         'grade_123',
         85,
         'Good work',
         ['Add more comments']
      )
    end

    before do
      allow(client.assignments).to receive().and_return(kai_result)
    end

    it 'grades the submission successfully' do
      result = service.grade_submission(assignment, submission)

      expect(result.score).to eq(85)
      expect(submission.reload.score).to eq(85)
      expect(submission.feedback).to eq('Good work')
    end

    context 'when grading fails' do
      before do
        allow(client.assignments).to receive()
          .and_raise(Kai::APIError.new('API error'))
      end

      it 'handles the error gracefully' do
        expect {
          service.grade_submission(assignment, submission)
        }.not_to raise_error

        expect(submission.reload.graded_at).to be_nil
      end
    end
  end
end