Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ The output will appear in `docs/build/html`.

## Code organization

A special case is the namespace `LaunchDarkly::AI::Impl`, and any namespaces within it. Everything under `Impl` is considered a private implementation detail: all files there are excluded from the generated documentation, and are considered subject to change at any time and not supported for direct use by application developers. We do this because Ruby's scope/visibility system is somewhat limited compared to other languages: a method can be `private` or `protected` within a class, but there is no way to make it visible to other classes in the library yet invisible to code outside of the library, and there is similarly no way to hide a class.
A special case is the namespace `LaunchDarkly::Server::AI::Impl`, and any namespaces within it. Everything under `Impl` is considered a private implementation detail: all files there are excluded from the generated documentation, and are considered subject to change at any time and not supported for direct use by application developers. We do this because Ruby's scope/visibility system is somewhat limited compared to other languages: a method can be `private` or `protected` within a class, but there is no way to make it visible to other classes in the library yet invisible to code outside of the library, and there is similarly no way to hide a class.

So, if there is a class whose existence is entirely an implementation detail, it should be in `Impl`. Similarly, classes that are _not_ in `Impl` must not expose any public members that are not meant to be part of the supported public API. This is important because of our guarantee of backward compatibility for all public APIs within a major version: we want to be able to change our implementation details to suit the needs of the code, without worrying about breaking a customer's code. Due to how the language works, we can't actually prevent an application developer from referencing those classes in their code, but this convention makes it clear that such use is discouraged and unsupported.

## Documenting types and methods

All classes and public methods outside of `LaunchDarkly::AI::Impl` should have documentation comments. These are used to build the API documentation that is published at https://launchdarkly.github.io/ruby-server-sdk-ai/ and https://www.rubydoc.info/gems/launchdarkly-server-sdk-ai. The documentation generator is YARD; see https://yardoc.org/ for the comment format it uses.
All classes and public methods outside of `LaunchDarkly::Server::AI::Impl` should have documentation comments. These are used to build the API documentation that is published at https://launchdarkly.github.io/ruby-server-sdk-ai/ and https://www.rubydoc.info/gems/launchdarkly-server-sdk-ai. The documentation generator is YARD; see https://yardoc.org/ for the comment format it uses.

Please try to make the style and terminology in documentation comments consistent with other documentation comments in the library. Also, if a class or method is being added that has an equivalent in other libraries, and if we have described it in a consistent away in those other libraries, please reuse the text whenever possible (with adjustments for anything language-specific) rather than writing new text.
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

source 'https://rubygems.org'

# Specify your gem's dependencies in launchdarkly-server-sdk-ai.gemspec
gemspec
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
6 changes: 6 additions & 0 deletions examples/hello-bedrock/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'aws-sdk-bedrockruntime'
gem 'launchdarkly-server-sdk-ai', path: '../../..'
43 changes: 27 additions & 16 deletions launchdarkly-server-sdk-ai.gemspec
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# frozen_string_literal: true

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

Gem::Specification.new do |spec|
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.name = 'launchdarkly-server-sdk-ai'
spec.version = LaunchDarkly::Server::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.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'
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.21'
spec.add_development_dependency 'rubocop-performance', '~> 1.15'
spec.add_development_dependency 'rubocop-rake', '~> 0.6'
spec.add_development_dependency 'rubocop-rspec', '~> 3.6'
end
29 changes: 28 additions & 1 deletion lib/launchdarkly-server-sdk-ai.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# frozen_string_literal: true

raise "Reserved for LaunchDarkly"
require 'logger'
require 'mustache'

require 'server/ai/version'
require 'server/ai/client'
require 'server/ai/config_tracker'

module LaunchDarkly
module Server
#
# 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
end
207 changes: 207 additions & 0 deletions lib/server/ai/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# frozen_string_literal: true

require 'ldclient-rb'
require 'mustache'
require_relative 'config_tracker'

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

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

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, nil] The parameter value or nil if not found
#
def parameter(key)
return @name if key == 'name'
return nil unless @parameters.is_a?(Hash)

@parameters[key]
end

#
# Retrieve customer provided data.
#
# @param key [String] The custom key to retrieve
# @return [Object, nil] The custom value or nil if not found
#
def custom(key)
return nil unless @custom.is_a?(Hash)

@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 Client class is the main entry point for the LaunchDarkly AI SDK.
#
class Client
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::Server::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
)

all_variables = variables ? variables.dup : {}
all_variables[:ldctx] = context.to_h

#
# Process messages and provider configuration
#
messages = nil
if variation[:messages].is_a?(Array) && variation[:messages].all? { |msg| msg.is_a?(Hash) }
messages = variation[:messages].map do |message|
next unless message[:content].is_a?(String)

Message.new(
message[:role],
Mustache.render(message[:content], all_variables)
)
end
end

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

if (model = variation[:model]) && 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::Server::AI::AIConfigTracker.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
end
Loading