-
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 all 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/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,245 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'ldclient-rb' | ||
|
||
module LaunchDarkly | ||
module Server | ||
module AI | ||
# | ||
# Tracks token usage for AI operations. | ||
# | ||
class TokenUsage | ||
attr_reader :total, :input, :output | ||
|
||
# | ||
# @param total [Integer] Total number of tokens used. | ||
# @param input [Integer] Number of tokens in the prompt. | ||
# @param output [Integer] Number of tokens in the completion. | ||
# | ||
def initialize(total: nil, input: nil, output: nil) | ||
@total = total | ||
@input = input | ||
@output = output | ||
end | ||
end | ||
|
||
# | ||
# Summary of metrics which have been tracked. | ||
# | ||
class MetricSummary | ||
attr_accessor :duration, :success, :feedback, :usage, :time_to_first_token | ||
|
||
def initialize | ||
@duration = nil | ||
@success = nil | ||
@feedback = nil | ||
@usage = nil | ||
@time_to_first_token = nil | ||
end | ||
end | ||
|
||
# | ||
# The AIConfigTracker class is used to track AI configuration usage. | ||
# | ||
class AIConfigTracker | ||
attr_reader :ld_client, :config_key, :context, :variation_key, :version, :summary | ||
|
||
def initialize(ld_client:, variation_key:, config_key:, version:, context:) | ||
@ld_client = ld_client | ||
@variation_key = variation_key | ||
@config_key = config_key | ||
@version = version | ||
@context = context | ||
@summary = MetricSummary.new | ||
end | ||
|
||
# | ||
# Track the duration of an AI operation | ||
# | ||
# @param duration [Integer] The duration in milliseconds | ||
# | ||
def track_duration(duration) | ||
@summary.duration = duration | ||
@ld_client.track( | ||
'$ld:ai:duration:total', | ||
@context, | ||
flag_data, | ||
duration | ||
) | ||
end | ||
|
||
# | ||
# Track the duration of a block of code | ||
# | ||
# @yield The block to measure | ||
# @return The result of the block | ||
# | ||
def track_duration_of(&block) | ||
start_time = Time.now | ||
yield | ||
ensure | ||
duration = ((Time.now - start_time) * 1000).to_i | ||
track_duration(duration) | ||
end | ||
|
||
# | ||
# Track time to first token | ||
# | ||
# @param duration [Integer] The duration in milliseconds | ||
# | ||
def track_time_to_first_token(time_to_first_token) | ||
@summary.time_to_first_token = time_to_first_token | ||
@ld_client.track( | ||
'$ld:ai:tokens:ttf', | ||
@context, | ||
flag_data, | ||
time_to_first_token | ||
) | ||
end | ||
|
||
# | ||
# Track user feedback | ||
# | ||
# @param kind [Symbol] The kind of feedback (:positive or :negative) | ||
# | ||
def track_feedback(kind:) | ||
@summary.feedback = kind | ||
event_name = kind == :positive ? '$ld:ai:feedback:user:positive' : '$ld:ai:feedback:user:negative' | ||
@ld_client.track( | ||
event_name, | ||
@context, | ||
flag_data, | ||
1 | ||
) | ||
end | ||
|
||
# | ||
# Track a successful AI generation | ||
# | ||
def track_success | ||
@summary.success = true | ||
@ld_client.track( | ||
'$ld:ai:generation', | ||
@context, | ||
flag_data, | ||
1 | ||
) | ||
@ld_client.track( | ||
'$ld:ai:generation:success', | ||
@context, | ||
flag_data, | ||
1 | ||
) | ||
end | ||
|
||
# | ||
# Track an error in AI generation | ||
# | ||
def track_error | ||
@summary.success = false | ||
@ld_client.track( | ||
'$ld:ai:generation', | ||
@context, | ||
flag_data, | ||
1 | ||
) | ||
@ld_client.track( | ||
'$ld:ai:generation:error', | ||
@context, | ||
flag_data, | ||
1 | ||
) | ||
end | ||
|
||
# | ||
# Track token usage | ||
# | ||
# @param token_usage [TokenUsage] An object containing token usage details | ||
# | ||
def track_tokens(token_usage) | ||
@summary.usage = token_usage | ||
if token_usage.total.positive? | ||
@ld_client.track( | ||
'$ld:ai:tokens:total', | ||
@context, | ||
flag_data, | ||
token_usage.total | ||
) | ||
end | ||
if token_usage.input.positive? | ||
@ld_client.track( | ||
'$ld:ai:tokens:input', | ||
@context, | ||
flag_data, | ||
token_usage.input | ||
) | ||
end | ||
return unless token_usage.output.positive? | ||
|
||
@ld_client.track( | ||
'$ld:ai:tokens:output', | ||
@context, | ||
flag_data, | ||
token_usage.output | ||
) | ||
end | ||
|
||
# | ||
# Track OpenAI-specific operations. | ||
# This method tracks the duration, token usage, and success/error status. | ||
# If the provided block raises, this method will also raise. | ||
# A failed operation will not have any token usage data. | ||
# | ||
# @yield The block to track. | ||
# @return The result of the tracked block. | ||
# | ||
def track_openai_metrics(&block) | ||
result = track_duration_of(&block) | ||
track_success | ||
track_tokens(openai_to_token_usage(result[:usage])) if result[:usage] | ||
result | ||
rescue StandardError | ||
track_error | ||
raise | ||
end | ||
|
||
# | ||
# Track AWS Bedrock conversation operations. | ||
# This method tracks the duration, token usage, and success/error status. | ||
# | ||
# @yield The block to track. | ||
# @return [Hash] The original response hash. | ||
# | ||
def track_bedrock_converse_metrics(&block) | ||
result = track_duration_of(&block) | ||
track_success | ||
track_tokens(bedrock_to_token_usage(result[:usage])) if result[:usage] | ||
result | ||
rescue StandardError | ||
track_error | ||
raise | ||
end | ||
|
||
private def flag_data | ||
{ variationKey: @variation_key, configKey: @config_key, version: @version } | ||
end | ||
|
||
private def openai_to_token_usage(usage) | ||
TokenUsage.new( | ||
total: usage[:total_tokens] || usage['total_tokens'], | ||
input: usage[:prompt_tokens] || usage['prompt_tokens'], | ||
output: usage[:completion_tokens] || usage['completion_tokens'] | ||
) | ||
end | ||
|
||
private def bedrock_to_token_usage(usage) | ||
TokenUsage.new( | ||
total: usage[:total_tokens] || usage['total_tokens'], | ||
input: usage[:input_tokens] || usage['input_tokens'], | ||
output: usage[:output_tokens] || usage['output_tokens'] | ||
) | ||
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.