launchdarkly-server-sdk-ai 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +54 -0
- data/PROVENANCE.md +43 -0
- data/README.md +34 -0
- data/SECURITY.md +5 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/launchdarkly-server-sdk-ai.rb +30 -1
- data/lib/server/ai/ai_config_tracker.rb +245 -0
- data/lib/server/ai/client.rb +205 -0
- data/lib/server/ai/version.rb +9 -0
- metadata +159 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4eca8fb4b72e683402ec14537df6b5570c2587004f9fbcd17d8cc9f7038dd46d
|
4
|
+
data.tar.gz: 0df3584ec493ea24d3d05e867be8243a1b336b94a72943b56f78e1cec6c524d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28792c2f4453c076312b8d9aa6cc56ce8b4fcadb2ad599ff73d0d6853bd037287b5d19ee38afffe0f98790fe7fe197c024ba863a719c1188d28efc041e9c8a6d
|
7
|
+
data.tar.gz: d116af21d8ecf7e40182d688d8eb1cf29780c15356d31e7fefcdb7ecbef4f2929b76586cf6906f5ac17e1cc807c7e359f546325aba0f48767791ea24c5b524bb
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [0.1.0](https://github.com/launchdarkly/ruby-server-sdk-ai/compare/0.0.0...0.1.0) (2025-06-18)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* Implement the AIClient and AITracker classes ([#1](https://github.com/launchdarkly/ruby-server-sdk-ai/issues/1)) ([7511fe9](https://github.com/launchdarkly/ruby-server-sdk-ai/commit/7511fe96e7eb9cec2140d0292fe251c2fb161840))
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# Contributing to the LaunchDarkly Server-side AI library for Ruby
|
2
|
+
|
3
|
+
LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this library.
|
4
|
+
|
5
|
+
## Submitting bug reports and feature requests
|
6
|
+
|
7
|
+
The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/ruby-server-sdk-ai/issues) in the library repository. Bug reports and feature requests specific to this library should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days.
|
8
|
+
|
9
|
+
## Submitting pull requests
|
10
|
+
|
11
|
+
We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days.
|
12
|
+
|
13
|
+
## Build instructions
|
14
|
+
|
15
|
+
### Prerequisites
|
16
|
+
|
17
|
+
This library is built with [Bundler](https://bundler.io/). To install Bundler, run `gem install bundler`. You might need `sudo` to execute the command successfully.
|
18
|
+
|
19
|
+
To install the runtime dependencies:
|
20
|
+
|
21
|
+
```
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
### Testing
|
26
|
+
|
27
|
+
To run all unit tests:
|
28
|
+
|
29
|
+
```
|
30
|
+
bundle exec rspec spec
|
31
|
+
```
|
32
|
+
|
33
|
+
### Building documentation
|
34
|
+
|
35
|
+
Documentation is built automatically with YARD for each release. To build the documentation locally:
|
36
|
+
|
37
|
+
```
|
38
|
+
cd docs
|
39
|
+
make
|
40
|
+
```
|
41
|
+
|
42
|
+
The output will appear in `docs/build/html`.
|
43
|
+
|
44
|
+
## Code organization
|
45
|
+
|
46
|
+
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.
|
47
|
+
|
48
|
+
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.
|
49
|
+
|
50
|
+
## Documenting types and methods
|
51
|
+
|
52
|
+
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.
|
53
|
+
|
54
|
+
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.
|
data/PROVENANCE.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
## Verifying SDK build provenance with the SLSA framework
|
2
|
+
|
3
|
+
LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages.
|
4
|
+
|
5
|
+
As part of [SLSA requirements for level 3 compliance](https://slsa.dev/spec/v1.0/requirements), LaunchDarkly publishes provenance about our SDK package builds using [GitHub's generic SLSA3 provenance generator](https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#generation-of-slsa3-provenance-for-arbitrary-projects) for distribution alongside our packages. These attestations are available for download from the GitHub release page for the release version under Assets > `multiple-provenance.intoto.jsonl`.
|
6
|
+
|
7
|
+
To verify SLSA provenance attestations, we recommend using [slsa-verifier](https://github.com/slsa-framework/slsa-verifier). Example usage for verifying SDK packages is included below:
|
8
|
+
|
9
|
+
<!-- x-release-please-start-version -->
|
10
|
+
```
|
11
|
+
# Set the version of the SDK to verify
|
12
|
+
VERSION=0.1.0
|
13
|
+
```
|
14
|
+
<!-- x-release-please-end -->
|
15
|
+
|
16
|
+
```
|
17
|
+
# Download gem
|
18
|
+
$ gem fetch launchdarkly-server-sdk-ai -v $VERSION
|
19
|
+
|
20
|
+
# Download provenance from Github release
|
21
|
+
$ curl --location -O \
|
22
|
+
https://github.com/launchdarkly/ruby-server-sdk-ai/releases/download/${VERSION}/launchdarkly-server-sdk-ai-${VERSION}.gem.intoto.jsonl
|
23
|
+
|
24
|
+
# Run slsa-verifier to verify provenance against package artifacts
|
25
|
+
$ slsa-verifier verify-artifact \
|
26
|
+
--provenance-path launchdarkly-server-sdk-ai-${VERSION}.gem.intoto.jsonl \
|
27
|
+
--source-uri github.com/launchdarkly/ruby-server-sdk-ai \
|
28
|
+
launchdarkly-server-sdk-ai-${VERSION}.gem
|
29
|
+
```
|
30
|
+
|
31
|
+
Below is a sample of expected output.
|
32
|
+
TODO: Verify these are accurate
|
33
|
+
```
|
34
|
+
Verified signature against tlog entry index 83653185 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a7df0bbf87a7d5fcaafa551a2101d9f993d251a56a918bb113e81d2c575dc7e25
|
35
|
+
Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.10.0" at commit 14c48a68c45871c27409591969e7f4c0ebdcdf62
|
36
|
+
Verifying artifact launchdarkly-server-sdk-ai-1.0.0.gem: PASSED
|
37
|
+
|
38
|
+
PASSED: Verified SLSA provenance
|
39
|
+
```
|
40
|
+
|
41
|
+
Alternatively, to verify the provenance manually, the SLSA framework specifies [recommendations for verifying build artifacts](https://slsa.dev/spec/v1.0/verifying-artifacts) in their documentation.
|
42
|
+
|
43
|
+
**Note:** These instructions do not apply when building our libraries from source.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
LaunchDarkly Server-side AI library for Ruby
|
2
|
+
==============================================
|
3
|
+
|
4
|
+
Learn more
|
5
|
+
-----------
|
6
|
+
|
7
|
+
Read our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [reference guide for the ruby SDK](http://docs.launchdarkly.com/docs/ruby-sdk-reference).
|
8
|
+
|
9
|
+
Generated API documentation for all versions of the library is on [RubyDoc.info](https://www.rubydoc.info/gems/launchdarkly-server-sdk-ai). The API documentation for the latest version is also on [GitHub Pages](https://launchdarkly.github.io/ruby-server-sdk-ai).
|
10
|
+
|
11
|
+
Contributing
|
12
|
+
------------
|
13
|
+
|
14
|
+
We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this library.
|
15
|
+
|
16
|
+
Verifying library build provenance with the SLSA framework
|
17
|
+
------------
|
18
|
+
|
19
|
+
LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published library packages. To learn more, see the [provenance guide](PROVENANCE.md).
|
20
|
+
|
21
|
+
About LaunchDarkly
|
22
|
+
-----------
|
23
|
+
|
24
|
+
* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
|
25
|
+
* Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
|
26
|
+
* Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
|
27
|
+
* Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
|
28
|
+
* Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
|
29
|
+
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
|
30
|
+
* Explore LaunchDarkly
|
31
|
+
* [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
|
32
|
+
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
|
33
|
+
* [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
|
34
|
+
* [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
|
data/SECURITY.md
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
# Reporting and Fixing Security Issues
|
2
|
+
|
3
|
+
Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty.
|
4
|
+
|
5
|
+
Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors.
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'launchdarkly_server_sdk_ai'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require 'irb'
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -1 +1,30 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'mustache'
|
5
|
+
|
6
|
+
require 'server/ai/version'
|
7
|
+
require 'server/ai/client'
|
8
|
+
require 'server/ai/ai_config_tracker'
|
9
|
+
|
10
|
+
module LaunchDarkly
|
11
|
+
module Server
|
12
|
+
#
|
13
|
+
# Namespace for the LaunchDarkly AI SDK.
|
14
|
+
#
|
15
|
+
module AI
|
16
|
+
#
|
17
|
+
# @return [Logger] the Rails logger if in Rails, or a default Logger at WARN level otherwise
|
18
|
+
#
|
19
|
+
def self.default_logger
|
20
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
21
|
+
Rails.logger
|
22
|
+
else
|
23
|
+
log = ::Logger.new($stdout)
|
24
|
+
log.level = ::Logger::WARN
|
25
|
+
log
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ldclient-rb'
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Server
|
7
|
+
module AI
|
8
|
+
#
|
9
|
+
# Tracks token usage for AI operations.
|
10
|
+
#
|
11
|
+
class TokenUsage
|
12
|
+
attr_reader :total, :input, :output
|
13
|
+
|
14
|
+
#
|
15
|
+
# @param total [Integer] Total number of tokens used.
|
16
|
+
# @param input [Integer] Number of tokens in the prompt.
|
17
|
+
# @param output [Integer] Number of tokens in the completion.
|
18
|
+
#
|
19
|
+
def initialize(total: nil, input: nil, output: nil)
|
20
|
+
@total = total
|
21
|
+
@input = input
|
22
|
+
@output = output
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Summary of metrics which have been tracked.
|
28
|
+
#
|
29
|
+
class MetricSummary
|
30
|
+
attr_accessor :duration, :success, :feedback, :usage, :time_to_first_token
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@duration = nil
|
34
|
+
@success = nil
|
35
|
+
@feedback = nil
|
36
|
+
@usage = nil
|
37
|
+
@time_to_first_token = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# The AIConfigTracker class is used to track AI configuration usage.
|
43
|
+
#
|
44
|
+
class AIConfigTracker
|
45
|
+
attr_reader :ld_client, :config_key, :context, :variation_key, :version, :summary
|
46
|
+
|
47
|
+
def initialize(ld_client:, variation_key:, config_key:, version:, context:)
|
48
|
+
@ld_client = ld_client
|
49
|
+
@variation_key = variation_key
|
50
|
+
@config_key = config_key
|
51
|
+
@version = version
|
52
|
+
@context = context
|
53
|
+
@summary = MetricSummary.new
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Track the duration of an AI operation
|
58
|
+
#
|
59
|
+
# @param duration [Integer] The duration in milliseconds
|
60
|
+
#
|
61
|
+
def track_duration(duration)
|
62
|
+
@summary.duration = duration
|
63
|
+
@ld_client.track(
|
64
|
+
'$ld:ai:duration:total',
|
65
|
+
@context,
|
66
|
+
flag_data,
|
67
|
+
duration
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Track the duration of a block of code
|
73
|
+
#
|
74
|
+
# @yield The block to measure
|
75
|
+
# @return The result of the block
|
76
|
+
#
|
77
|
+
def track_duration_of(&block)
|
78
|
+
start_time = Time.now
|
79
|
+
yield
|
80
|
+
ensure
|
81
|
+
duration = ((Time.now - start_time) * 1000).to_i
|
82
|
+
track_duration(duration)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Track time to first token
|
87
|
+
#
|
88
|
+
# @param duration [Integer] The duration in milliseconds
|
89
|
+
#
|
90
|
+
def track_time_to_first_token(time_to_first_token)
|
91
|
+
@summary.time_to_first_token = time_to_first_token
|
92
|
+
@ld_client.track(
|
93
|
+
'$ld:ai:tokens:ttf',
|
94
|
+
@context,
|
95
|
+
flag_data,
|
96
|
+
time_to_first_token
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Track user feedback
|
102
|
+
#
|
103
|
+
# @param kind [Symbol] The kind of feedback (:positive or :negative)
|
104
|
+
#
|
105
|
+
def track_feedback(kind:)
|
106
|
+
@summary.feedback = kind
|
107
|
+
event_name = kind == :positive ? '$ld:ai:feedback:user:positive' : '$ld:ai:feedback:user:negative'
|
108
|
+
@ld_client.track(
|
109
|
+
event_name,
|
110
|
+
@context,
|
111
|
+
flag_data,
|
112
|
+
1
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Track a successful AI generation
|
118
|
+
#
|
119
|
+
def track_success
|
120
|
+
@summary.success = true
|
121
|
+
@ld_client.track(
|
122
|
+
'$ld:ai:generation',
|
123
|
+
@context,
|
124
|
+
flag_data,
|
125
|
+
1
|
126
|
+
)
|
127
|
+
@ld_client.track(
|
128
|
+
'$ld:ai:generation:success',
|
129
|
+
@context,
|
130
|
+
flag_data,
|
131
|
+
1
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Track an error in AI generation
|
137
|
+
#
|
138
|
+
def track_error
|
139
|
+
@summary.success = false
|
140
|
+
@ld_client.track(
|
141
|
+
'$ld:ai:generation',
|
142
|
+
@context,
|
143
|
+
flag_data,
|
144
|
+
1
|
145
|
+
)
|
146
|
+
@ld_client.track(
|
147
|
+
'$ld:ai:generation:error',
|
148
|
+
@context,
|
149
|
+
flag_data,
|
150
|
+
1
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# Track token usage
|
156
|
+
#
|
157
|
+
# @param token_usage [TokenUsage] An object containing token usage details
|
158
|
+
#
|
159
|
+
def track_tokens(token_usage)
|
160
|
+
@summary.usage = token_usage
|
161
|
+
if token_usage.total.positive?
|
162
|
+
@ld_client.track(
|
163
|
+
'$ld:ai:tokens:total',
|
164
|
+
@context,
|
165
|
+
flag_data,
|
166
|
+
token_usage.total
|
167
|
+
)
|
168
|
+
end
|
169
|
+
if token_usage.input.positive?
|
170
|
+
@ld_client.track(
|
171
|
+
'$ld:ai:tokens:input',
|
172
|
+
@context,
|
173
|
+
flag_data,
|
174
|
+
token_usage.input
|
175
|
+
)
|
176
|
+
end
|
177
|
+
return unless token_usage.output.positive?
|
178
|
+
|
179
|
+
@ld_client.track(
|
180
|
+
'$ld:ai:tokens:output',
|
181
|
+
@context,
|
182
|
+
flag_data,
|
183
|
+
token_usage.output
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
#
|
188
|
+
# Track OpenAI-specific operations.
|
189
|
+
# This method tracks the duration, token usage, and success/error status.
|
190
|
+
# If the provided block raises, this method will also raise.
|
191
|
+
# A failed operation will not have any token usage data.
|
192
|
+
#
|
193
|
+
# @yield The block to track.
|
194
|
+
# @return The result of the tracked block.
|
195
|
+
#
|
196
|
+
def track_openai_metrics(&block)
|
197
|
+
result = track_duration_of(&block)
|
198
|
+
track_success
|
199
|
+
track_tokens(openai_to_token_usage(result[:usage])) if result[:usage]
|
200
|
+
result
|
201
|
+
rescue StandardError
|
202
|
+
track_error
|
203
|
+
raise
|
204
|
+
end
|
205
|
+
|
206
|
+
#
|
207
|
+
# Track AWS Bedrock conversation operations.
|
208
|
+
# This method tracks the duration, token usage, and success/error status.
|
209
|
+
#
|
210
|
+
# @yield The block to track.
|
211
|
+
# @return [Hash] The original response hash.
|
212
|
+
#
|
213
|
+
def track_bedrock_converse_metrics(&block)
|
214
|
+
result = track_duration_of(&block)
|
215
|
+
track_success
|
216
|
+
track_tokens(bedrock_to_token_usage(result[:usage])) if result[:usage]
|
217
|
+
result
|
218
|
+
rescue StandardError
|
219
|
+
track_error
|
220
|
+
raise
|
221
|
+
end
|
222
|
+
|
223
|
+
private def flag_data
|
224
|
+
{ variationKey: @variation_key, configKey: @config_key, version: @version }
|
225
|
+
end
|
226
|
+
|
227
|
+
private def openai_to_token_usage(usage)
|
228
|
+
TokenUsage.new(
|
229
|
+
total: usage[:total_tokens] || usage['total_tokens'],
|
230
|
+
input: usage[:prompt_tokens] || usage['prompt_tokens'],
|
231
|
+
output: usage[:completion_tokens] || usage['completion_tokens']
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
private def bedrock_to_token_usage(usage)
|
236
|
+
TokenUsage.new(
|
237
|
+
total: usage[:total_tokens] || usage['total_tokens'],
|
238
|
+
input: usage[:input_tokens] || usage['input_tokens'],
|
239
|
+
output: usage[:output_tokens] || usage['output_tokens']
|
240
|
+
)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ldclient-rb'
|
4
|
+
require 'mustache'
|
5
|
+
require_relative 'ai_config_tracker'
|
6
|
+
|
7
|
+
module LaunchDarkly
|
8
|
+
#
|
9
|
+
# Namespace for the LaunchDarkly Server SDK
|
10
|
+
#
|
11
|
+
module Server
|
12
|
+
#
|
13
|
+
# Namespace for the LaunchDarkly Server AI SDK.
|
14
|
+
#
|
15
|
+
module AI
|
16
|
+
#
|
17
|
+
# Holds AI role and content.
|
18
|
+
#
|
19
|
+
class Message
|
20
|
+
attr_reader :role, :content
|
21
|
+
|
22
|
+
def initialize(role, content)
|
23
|
+
@role = role
|
24
|
+
@content = content
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
{
|
29
|
+
role: @role,
|
30
|
+
content: @content,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# The ModelConfig class represents an AI model configuration.
|
37
|
+
#
|
38
|
+
class ModelConfig
|
39
|
+
attr_reader :name
|
40
|
+
|
41
|
+
def initialize(name:, parameters: {}, custom: {})
|
42
|
+
@name = name
|
43
|
+
@parameters = parameters
|
44
|
+
@custom = custom
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Retrieve model-specific parameters.
|
49
|
+
#
|
50
|
+
# Accessing a named, typed attribute (e.g. name) will result in the call
|
51
|
+
# being delegated to the appropriate property.
|
52
|
+
#
|
53
|
+
# @param key [String] The parameter key to retrieve
|
54
|
+
# @return [Object, nil] The parameter value or nil if not found
|
55
|
+
#
|
56
|
+
def parameter(key)
|
57
|
+
return @name if key == 'name'
|
58
|
+
return nil unless @parameters.is_a?(Hash)
|
59
|
+
|
60
|
+
@parameters[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Retrieve customer provided data.
|
65
|
+
#
|
66
|
+
# @param key [String] The custom key to retrieve
|
67
|
+
# @return [Object, nil] The custom value or nil if not found
|
68
|
+
#
|
69
|
+
def custom(key)
|
70
|
+
return nil unless @custom.is_a?(Hash)
|
71
|
+
|
72
|
+
@custom[key]
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_h
|
76
|
+
{
|
77
|
+
name: @name,
|
78
|
+
parameters: @parameters,
|
79
|
+
custom: @custom,
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Configuration related to the provider.
|
86
|
+
#
|
87
|
+
class ProviderConfig
|
88
|
+
attr_reader :name
|
89
|
+
|
90
|
+
def initialize(name)
|
91
|
+
@name = name
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_h
|
95
|
+
{
|
96
|
+
name: @name,
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# The AIConfig class represents an AI configuration.
|
103
|
+
#
|
104
|
+
class AIConfig
|
105
|
+
attr_reader :enabled, :messages, :variables, :tracker, :model, :provider
|
106
|
+
|
107
|
+
def initialize(enabled: nil, model: nil, messages: nil, tracker: nil, provider: nil)
|
108
|
+
@enabled = enabled
|
109
|
+
@messages = messages
|
110
|
+
@tracker = tracker
|
111
|
+
@model = model
|
112
|
+
@provider = provider
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_h
|
116
|
+
{
|
117
|
+
_ldMeta: {
|
118
|
+
enabled: @enabled || false,
|
119
|
+
},
|
120
|
+
messages: @messages.is_a?(Array) ? @messages.map { |msg| msg&.to_h } : nil,
|
121
|
+
model: @model&.to_h,
|
122
|
+
provider: @provider&.to_h,
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# The Client class is the main entry point for the LaunchDarkly AI SDK.
|
129
|
+
#
|
130
|
+
class Client
|
131
|
+
attr_reader :logger, :ld_client
|
132
|
+
|
133
|
+
def initialize(ld_client)
|
134
|
+
raise ArgumentError, 'LDClient instance is required' unless ld_client.is_a?(LaunchDarkly::LDClient)
|
135
|
+
|
136
|
+
@ld_client = ld_client
|
137
|
+
@logger = LaunchDarkly::Server::AI.default_logger
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Retrieves the AIConfig
|
142
|
+
#
|
143
|
+
# @param config_key [String] The key of the configuration flag
|
144
|
+
# @param context [LDContext] The context used when evaluating the flag
|
145
|
+
# @param default_value [AIConfig] The default value to use if the flag is not found
|
146
|
+
# @param variables [Hash] Optional variables for rendering messages
|
147
|
+
# @return [AIConfig] An AIConfig instance containing the configuration data
|
148
|
+
#
|
149
|
+
def config(config_key, context, default_value = nil, variables = nil)
|
150
|
+
variation = @ld_client.variation(
|
151
|
+
config_key,
|
152
|
+
context,
|
153
|
+
default_value.respond_to?(:to_h) ? default_value.to_h : nil
|
154
|
+
)
|
155
|
+
|
156
|
+
all_variables = variables ? variables.dup : {}
|
157
|
+
all_variables[:ldctx] = context.to_h
|
158
|
+
|
159
|
+
# Process messages and provider configuration
|
160
|
+
messages = nil
|
161
|
+
if variation[:messages].is_a?(Array) && variation[:messages].all? { |msg| msg.is_a?(Hash) }
|
162
|
+
messages = variation[:messages].map do |message|
|
163
|
+
next unless message[:content].is_a?(String)
|
164
|
+
|
165
|
+
Message.new(
|
166
|
+
message[:role],
|
167
|
+
Mustache.render(message[:content], all_variables)
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
if (provider_config = variation[:provider]) && provider_config.is_a?(Hash)
|
173
|
+
provider_config = ProviderConfig.new(provider_config.fetch(:name, ''))
|
174
|
+
end
|
175
|
+
|
176
|
+
if (model = variation[:model]) && model.is_a?(Hash)
|
177
|
+
parameters = variation[:model][:parameters]
|
178
|
+
custom = variation[:model][:custom]
|
179
|
+
model = ModelConfig.new(
|
180
|
+
name: variation[:model][:name],
|
181
|
+
parameters: parameters,
|
182
|
+
custom: custom
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
tracker = LaunchDarkly::Server::AI::AIConfigTracker.new(
|
187
|
+
ld_client: @ld_client,
|
188
|
+
variation_key: variation.dig(:_ldMeta, :variationKey) || '',
|
189
|
+
config_key: config_key,
|
190
|
+
version: variation.dig(:_ldMeta, :version) || 1,
|
191
|
+
context: context
|
192
|
+
)
|
193
|
+
|
194
|
+
AIConfig.new(
|
195
|
+
enabled: variation.dig(:_ldMeta, :enabled) || false,
|
196
|
+
messages: messages,
|
197
|
+
tracker: tracker,
|
198
|
+
model: model,
|
199
|
+
provider: provider_config
|
200
|
+
)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
metadata
CHANGED
@@ -1,25 +1,180 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: launchdarkly-server-sdk-ai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: launchdarkly-server-sdk
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '8.5'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '8.5'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: logger
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: mustache
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.1'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.1'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: bundler
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rake
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '13.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '13.0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rspec
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rubocop
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.21'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.21'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rubocop-performance
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.15'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '1.15'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: rubocop-rake
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0.6'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0.6'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: rubocop-rspec
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '3.6'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '3.6'
|
12
152
|
description: LaunchDarkly SDK AI Configs integration for the Ruby server side SDK
|
13
153
|
email:
|
14
154
|
- team@launchdarkly.com
|
15
|
-
executables:
|
155
|
+
executables:
|
156
|
+
- console
|
157
|
+
- setup
|
16
158
|
extensions: []
|
17
159
|
extra_rdoc_files: []
|
18
160
|
files:
|
161
|
+
- CHANGELOG.md
|
162
|
+
- CONTRIBUTING.md
|
163
|
+
- PROVENANCE.md
|
164
|
+
- README.md
|
165
|
+
- SECURITY.md
|
166
|
+
- bin/console
|
167
|
+
- bin/setup
|
19
168
|
- lib/launchdarkly-server-sdk-ai.rb
|
169
|
+
- lib/server/ai/ai_config_tracker.rb
|
170
|
+
- lib/server/ai/client.rb
|
171
|
+
- lib/server/ai/version.rb
|
172
|
+
homepage: https://github.com/launchdarkly/ruby-server-sdk-ai
|
20
173
|
licenses:
|
21
174
|
- Apache-2.0
|
22
|
-
metadata:
|
175
|
+
metadata:
|
176
|
+
source_code_uri: https://github.com/launchdarkly/ruby-server-sdk-ai
|
177
|
+
changelog_uri: https://github.com/launchdarkly/ruby-server-sdk-ai/blob/main/CHANGELOG.md
|
23
178
|
rdoc_options: []
|
24
179
|
require_paths:
|
25
180
|
- lib
|