Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ doc/
/pkg/
/spec/reports/
/tmp/

.DS_Store
# rspec failure tracking
.rspec_status

Expand Down
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
15 changes: 15 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

source "https://rubygems.org"

# Specify your gem's dependencies in launchdarkly-server-sdk-ai.gemspec
gemspec

gem "rake", "~> 13.0"

gem "rspec", "~> 3.0"

gem "rubocop", "~> 1.21"
gem "rubocop-performance", "~> 1.15"
gem "rubocop-rake", "~> 0.6"
gem "rubocop-rspec", "~> 2.27"
11 changes: 11 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'bundler/setup'
require 'launchdarkly_server_sdk_ai'

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

require 'irb'
IRB.start(__FILE__)
8 changes: 8 additions & 0 deletions bin/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx

bundle install

# Do any other automated setup that you need to do here
39 changes: 24 additions & 15 deletions launchdarkly-server-sdk-ai.gemspec
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
# frozen_string_literal: true

require_relative "lib/ldclient-ai/version"
require_relative 'lib/ldclient-ai/version'

Gem::Specification.new do |spec|
spec.name = "launchdarkly-server-sdk-ai"
spec.name = 'launchdarkly-server-sdk-ai'
spec.version = LaunchDarkly::AI::VERSION
spec.authors = ["LaunchDarkly"]
spec.email = ["[email protected]"]
spec.summary = "LaunchDarkly AI SDK for Ruby"
spec.description = "LaunchDarkly SDK AI Configs integration for the Ruby server side SDK"
spec.license = "Apache-2.0"
spec.homepage = "https://github.com/launchdarkly/ruby-server-sdk-ai"
spec.metadata["source_code_uri"] = "https://github.com/launchdarkly/ruby-server-sdk-ai"
spec.metadata["changelog_uri"] = "https://github.com/launchdarkly/ruby-server-sdk-ai/blob/main/CHANGELOG.md"
spec.authors = ['LaunchDarkly']
spec.email = ['[email protected]']
spec.summary = 'LaunchDarkly AI SDK for Ruby'
spec.description = 'LaunchDarkly SDK AI Configs integration for the Ruby server side SDK'
spec.license = 'Apache-2.0'
spec.homepage = 'https://github.com/launchdarkly/ruby-server-sdk-ai'
spec.metadata['source_code_uri'] = 'https://github.com/launchdarkly/ruby-server-sdk-ai'
spec.metadata['changelog_uri'] = 'https://github.com/launchdarkly/ruby-server-sdk-ai/blob/main/CHANGELOG.md'

spec.files = Dir["{lib}/**/*.rb", "bin/*", "LICENSE", "*.md"]
spec.files = Dir['{lib}/**/*.rb', 'bin/*', 'LICENSE', '*.md']
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.required_ruby_version = ">= 3.0.0"
spec.require_paths = ['lib']
spec.required_ruby_version = '>= 3.0.0'

spec.add_runtime_dependency "launchdarkly-server-sdk", "~> 8.4.0"
end
spec.add_dependency 'launchdarkly-server-sdk', '~> 8.5.0'
spec.add_dependency 'logger'
spec.add_dependency 'mustache', '~> 1.1'

spec.add_development_dependency 'bundler', '~> 2.0'
spec.add_development_dependency 'debug', '~> 1.0'
spec.add_development_dependency 'rake', '~> 13.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rubocop', '~> 1.0'
spec.add_development_dependency 'rubocop-rspec', '~> 2.0'
end
3 changes: 0 additions & 3 deletions lib/launchdarkly-server-sdk-ai.rb

This file was deleted.

3 changes: 3 additions & 0 deletions lib/launchdarkly_server_sdk_ai.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

require_relative 'ldclient-ai'
28 changes: 28 additions & 0 deletions lib/ldclient-ai.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require 'logger'
require 'mustache'

require 'ldclient-ai/version'
require 'ldclient-ai/ld_ai_client'
require 'ldclient-ai/ld_ai_config_tracker'

module LaunchDarkly
#
# Namespace for the LaunchDarkly AI SDK.
#
module AI
#
# @return [Logger] the Rails logger if in Rails, or a default Logger at WARN level otherwise
#
def self.default_logger
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
Rails.logger
else
log = ::Logger.new($stdout)
log.level = ::Logger::WARN
log
end
end
end
end
179 changes: 179 additions & 0 deletions lib/ldclient-ai/ld_ai_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# frozen_string_literal: true

require 'ldclient-rb'
require 'mustache'
require_relative 'ld_ai_config_tracker'

module LaunchDarkly
#
# Namespace for the LaunchDarkly AI SDK.
#
module AI
# Holds AI role and content.
class LDMessage
attr_reader :role, :content

# TODO: Do we need to validate the role to only be 'system', 'user', or 'assistant'?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this on Slack at one point, but following up here that I still don't think you need to worry about that.

def initialize(role, content)
@role = role
@content = content
end

def to_h
{
role: @role,
content: @content
}
end
end

# The ModelConfig class represents an AI model configuration.
class ModelConfig
attr_reader :name, :parameters, :custom
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you split these into individual attr_reader lines, you can provide documentation. There is an example here.

Or given that you are providing those getters below, maybe you should actually remove :parameters, and :custom. That would align with the Python AI SDK better.


def initialize(name:, parameters: {}, custom: {})
@name = name
@parameters = parameters
@custom = custom
end

# Retrieve model-specific parameters.
#
# Accessing a named, typed attribute (e.g. name) will result in the call
# being delegated to the appropriate property.
#
# @param key [String] The parameter key to retrieve
# @return [Object] The parameter value or nil if not found
def get_parameter(key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you get read of the attr_reader methods, then renaming this to def parameter(key) would seem nice.

return @name if key == 'name'
return nil if @parameters.nil?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could change this to return nil unless @parameters.is_a?(Hash) Then you cover the nil and invalid case in one.


@parameters[key]
end

# Retrieve customer provided data.
#
# @param key [String] The custom key to retrieve
# @return [Object] The custom value or nil if not found
def get_custom(key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with making this def custom(key)

return nil if @custom.nil?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal here with the .is_a?(Hash) trick.


@custom[key]
end

def to_h
{
name: @name,
parameters: @parameters,
custom: @custom
}
end
end

# Configuration related to the provider.
class ProviderConfig
attr_reader :name

def initialize(name)
@name = name
end

def to_h
{
name: @name
}
end
end

# The AIConfig class represents an AI configuration.
class AIConfig
attr_reader :enabled, :messages, :variables, :tracker, :model, :provider

def initialize(enabled: nil, model: nil, messages: nil, tracker: nil, provider: nil)
@enabled = enabled
@messages = messages
@tracker = tracker
@model = model
@provider = provider
end

def to_h
{
_ldMeta: {
enabled: @enabled || false
},
messages: @messages.is_a?(Array) ? @messages.map { |msg| msg&.to_h } : nil,
model: @model&.to_h,
provider: @provider&.to_h
}
end
end

# The LDAIClient class is the main entry point for the LaunchDarkly AI SDK.
class LDAIClient
attr_reader :logger, :ld_client

def initialize(ld_client)
raise ArgumentError, 'LDClient instance is required' unless ld_client.is_a?(LaunchDarkly::LDClient)

@ld_client = ld_client
@logger = LaunchDarkly::AI.default_logger
end

# Retrieves the AIConfig
# @param config_key [String] The key of the configuration flag
# @param context [LDContext] The context used when evaluating the flag
# @param default_value [AIConfig] The default value to use if the flag is not found
# @param variables [Hash] Optional variables for rendering messages
# @return [AIConfig] An AIConfig instance containing the configuration data
def config(config_key, context, default_value = nil, variables = nil)
variation = @ld_client.variation(
config_key,
context,
default_value.respond_to?(:to_h) ? default_value.to_h : nil
)

variables ||= {}
variables[:ldctx] = context.to_h

messages = variation.fetch(:messages, nil)
if messages.is_a?(Array) && messages.all? { |msg| msg.is_a?(Hash) }
messages = messages.map do |message|
message[:content] = Mustache.render(message[:content], variables) if message[:content].is_a?(String)
message
end
end

if (provider_config = variation.fetch(:provider, nil)) && provider_config.is_a?(Hash)
provider_config = ProviderConfig.new(provider_config.fetch(:name, ''))
end

if (model = variation.fetch(:model, nil)) && model.is_a?(Hash)
parameters = variation[:model][:parameters]
custom = variation[:model][:custom]
model = ModelConfig.new(
name: variation[:model][:name],
parameters: parameters,
custom: custom
)
end

tracker = LaunchDarkly::AI::LDAIConfigTracker.new(
ld_client: @ld_client,
variation_key: variation.dig(:_ldMeta, :variationKey) || '',
config_key: config_key,
version: variation.dig(:_ldMeta, :version) || 1,
context: context
)

AIConfig.new(
enabled: variation.dig(:_ldMeta, :enabled) || false,
messages: messages,
tracker: tracker,
model: model,
provider: provider_config
)
end
end
end
end
Loading