-
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
Merged
jsonbailey
merged 20 commits into
main
from
jb/sdk-1273/create-ldaiclient-ldaiconfig-tracker
Jun 17, 2025
Merged
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
ceec84c
feat: Support the LDAIClient
jsonbailey 9dbe735
giving cursor a go at developing the library
jsonbailey c246ead
Working to get running tests, still failing
jsonbailey b946c2e
Letting cursor have another go
jsonbailey 01f1503
fix the data source created by cursor
jsonbailey 5dfbbe8
finish adding all the client tests and have them passing
jsonbailey ca59de7
all ai client tests are passing
jsonbailey ec54093
fixing issues in the tracker
jsonbailey 1d7d6f7
fix remaining tests for the tracker
jsonbailey 6399b69
gemfile updates
jsonbailey ec553d5
Address code review feedback
devin-ai-integration[bot] 1bb2f01
Addressing feedback
jsonbailey 3585f84
don't modify passed in objects
jsonbailey 6acd89b
Use the proper variables for rendering
jsonbailey d8972ee
Apply suggestions from code review
jsonbailey f6df663
Update module naming to align more closely to package
jsonbailey 305483c
fix some style issues
jsonbailey ac5b07b
align bedrock with spec
jsonbailey 9135945
Update lib/server/ai/client.rb
jsonbailey 2c1f8b1
align file name with class name
jsonbailey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
--format documentation | ||
--color | ||
--require spec_helper |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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__) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: '../../..' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
# | ||
jsonbailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.