-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Implement the AIClient and AITracker classes #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
ceec84c
9dbe735
c246ead
b946c2e
01f1503
5dfbbe8
ca59de7
ec54093
1d7d6f7
6399b69
ec553d5
1bb2f01
3585f84
6acd89b
d8972ee
f6df663
305483c
ac5b07b
9135945
2c1f8b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ doc/ | |
/pkg/ | ||
/spec/reports/ | ||
/tmp/ | ||
|
||
.DS_Store | ||
# rspec failure tracking | ||
.rspec_status | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
--format documentation | ||
--color | ||
--require spec_helper |
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" | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
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__) |
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 |
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 |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'ldclient-ai' | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
# frozen_string_literal: true | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class LDMessage | ||
attr_reader :role, :content | ||
|
||
# TODO: Do we need to validate the role to only be 'system', 'user', or 'assistant'? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you split these into individual Or given that you are providing those getters below, maybe you should actually remove |
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you get read of the |
||
return @name if key == 'name' | ||
return nil if @parameters.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could change this to |
||
|
||
@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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same with making this |
||
return nil if @custom.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same deal here with the |
||
|
||
@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) | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
provider_config = ProviderConfig.new(provider_config.fetch(:name, '')) | ||
end | ||
|
||
if (model = variation.fetch(:model, nil)) && model.is_a?(Hash) | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 |
Uh oh!
There was an error while loading. Please reload this page.