langsmithrb_rails 0.1.0 → 0.3.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/.rspec +3 -0
- data/.rspec_status +161 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +321 -0
- data/LICENSE +21 -0
- data/README.md +421 -0
- data/Rakefile +10 -0
- data/langsmithrb_rails-0.1.0.gem +0 -0
- data/langsmithrb_rails-0.1.1.gem +0 -0
- data/langsmithrb_rails.gemspec +45 -0
- data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
- data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
- data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
- data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
- data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
- data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
- data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
- data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
- data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
- data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
- data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
- data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
- data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
- data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
- data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
- data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
- data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
- data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
- data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
- data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
- data/lib/langsmithrb_rails/client.rb +292 -0
- data/lib/langsmithrb_rails/config.rb +169 -0
- data/lib/langsmithrb_rails/evaluation/evaluator.rb +178 -0
- data/lib/langsmithrb_rails/evaluation/llm_evaluator.rb +154 -0
- data/lib/langsmithrb_rails/evaluation/string_evaluator.rb +158 -0
- data/lib/langsmithrb_rails/evaluation.rb +76 -0
- data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
- data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
- data/lib/langsmithrb_rails/langsmith.rb +35 -0
- data/lib/langsmithrb_rails/otel/exporter.rb +120 -0
- data/lib/langsmithrb_rails/otel.rb +135 -0
- data/lib/langsmithrb_rails/railtie.rb +33 -0
- data/lib/langsmithrb_rails/redactor.rb +76 -0
- data/lib/langsmithrb_rails/run_trees.rb +157 -0
- data/lib/langsmithrb_rails/version.rb +5 -0
- data/lib/langsmithrb_rails/wrappers/anthropic.rb +146 -0
- data/lib/langsmithrb_rails/wrappers/base.rb +81 -0
- data/lib/langsmithrb_rails/wrappers/llm.rb +151 -0
- data/lib/langsmithrb_rails/wrappers/openai.rb +193 -0
- data/lib/langsmithrb_rails/wrappers.rb +41 -0
- data/lib/langsmithrb_rails.rb +151 -0
- data/pkg/langsmithrb_rails-0.3.0.gem +0 -0
- metadata +74 -7
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LangsmithrbRails
|
4
|
+
module Generators
|
5
|
+
# Generator for adding LangSmith tracing to Rails applications
|
6
|
+
class TracingGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
|
9
|
+
desc "Adds LangSmith tracing to your Rails application"
|
10
|
+
|
11
|
+
def create_middleware
|
12
|
+
template "request_tracing.rb", "app/middleware/langsmithrb_rails/request_tracing.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_service_concern
|
16
|
+
template "langsmith_traced.rb", "app/services/concerns/langsmith_traced.rb"
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_job_concern
|
20
|
+
template "langsmith_traced_job.rb", "app/jobs/concerns/langsmith_traced_job.rb"
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_application_config
|
24
|
+
application_rb_path = "config/application.rb"
|
25
|
+
|
26
|
+
if File.exist?(application_rb_path)
|
27
|
+
middleware_line = " config.middleware.use LangsmithrbRails::RequestTracing"
|
28
|
+
|
29
|
+
# Check if middleware is already configured
|
30
|
+
if File.read(application_rb_path).include?(middleware_line)
|
31
|
+
say_status :skip, "Middleware already configured in application.rb", :yellow
|
32
|
+
else
|
33
|
+
# Find the class Application < Rails::Application line
|
34
|
+
application_content = File.read(application_rb_path)
|
35
|
+
|
36
|
+
if application_content =~ /class Application < Rails::Application/
|
37
|
+
inject_into_file application_rb_path, after: "class Application < Rails::Application\n" do
|
38
|
+
<<~RUBY
|
39
|
+
# Use LangSmith request tracing middleware
|
40
|
+
#{middleware_line}
|
41
|
+
|
42
|
+
RUBY
|
43
|
+
end
|
44
|
+
|
45
|
+
say_status :insert, "Added middleware to application.rb", :green
|
46
|
+
else
|
47
|
+
say_status :error, "Could not find the Application class in application.rb", :red
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
say_status :error, "Could not find application.rb", :red
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_directories
|
56
|
+
empty_directory "app/middleware/langsmithrb_rails"
|
57
|
+
empty_directory "app/services/concerns"
|
58
|
+
empty_directory "app/jobs/concerns"
|
59
|
+
end
|
60
|
+
|
61
|
+
def display_post_install_message
|
62
|
+
say "\n"
|
63
|
+
say "LangSmith tracing has been added to your Rails application! 🎉", :green
|
64
|
+
say "\n"
|
65
|
+
say "Usage:", :yellow
|
66
|
+
say " 1. Use the LangsmithTraced concern in your services:", :yellow
|
67
|
+
say " include LangsmithTraced", :yellow
|
68
|
+
say " LangsmithTraced.trace(name: 'my_operation', type: 'llm') { ... }", :yellow
|
69
|
+
say " 2. Use the LangsmithTracedJob concern in your jobs:", :yellow
|
70
|
+
say " include LangsmithTracedJob", :yellow
|
71
|
+
say "\n"
|
72
|
+
say "To add a local buffer for traces, run:", :yellow
|
73
|
+
say " bin/rails g langsmithrb_rails:buffer", :yellow
|
74
|
+
say "\n"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
module LangsmithrbRails
|
9
|
+
# Direct REST client for LangSmith API
|
10
|
+
class Client
|
11
|
+
# Initialize a new LangSmith client
|
12
|
+
# @param api_key [String] LangSmith API key
|
13
|
+
# @param api_url [String] LangSmith API URL
|
14
|
+
def initialize(api_key: Config[:api_key], api_url: Config[:api_url])
|
15
|
+
@api_key = api_key
|
16
|
+
@api_url = api_url.chomp("/")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create a new run in LangSmith
|
20
|
+
# @param payload [Hash] Run data
|
21
|
+
# @return [Hash] Response with status and body
|
22
|
+
def create_run(payload)
|
23
|
+
post("/runs", payload)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Update an existing run in LangSmith
|
27
|
+
# @param id [String] Run ID
|
28
|
+
# @param payload [Hash] Updated run data
|
29
|
+
# @return [Hash] Response with status and body
|
30
|
+
def update_run(id, payload)
|
31
|
+
patch("/runs/#{id}", payload)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a run by ID
|
35
|
+
# @param id [String] Run ID
|
36
|
+
# @return [Hash] Response with status and body
|
37
|
+
def get_run(id)
|
38
|
+
get("/runs/#{id}")
|
39
|
+
end
|
40
|
+
|
41
|
+
# List runs with optional filters
|
42
|
+
# @param project_name [String] Filter by project name
|
43
|
+
# @param trace_id [String] Filter by trace ID
|
44
|
+
# @param run_type [String] Filter by run type
|
45
|
+
# @param limit [Integer] Maximum number of runs to return
|
46
|
+
# @param offset [Integer] Offset for pagination
|
47
|
+
# @return [Hash] Response with status and body
|
48
|
+
def list_runs(project_name: nil, trace_id: nil, run_type: nil, limit: 100, offset: 0)
|
49
|
+
query_params = {
|
50
|
+
project_name: project_name,
|
51
|
+
trace_id: trace_id,
|
52
|
+
run_type: run_type,
|
53
|
+
limit: limit,
|
54
|
+
offset: offset
|
55
|
+
}.compact
|
56
|
+
|
57
|
+
query_string = query_params.map { |k, v| "#{k}=#{URI.encode_www_form_component(v.to_s)}" }.join("&")
|
58
|
+
get("/runs?#{query_string}")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create a dataset in LangSmith
|
62
|
+
# @param name [String] Dataset name
|
63
|
+
# @param description [String] Dataset description
|
64
|
+
# @return [Hash] Response with status and body
|
65
|
+
def create_dataset(name, description: nil)
|
66
|
+
payload = {
|
67
|
+
name: name,
|
68
|
+
description: description
|
69
|
+
}.compact
|
70
|
+
post("/datasets", payload)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get a dataset by ID
|
74
|
+
# @param id [String] Dataset ID
|
75
|
+
# @return [Hash] Response with status and body
|
76
|
+
def get_dataset(id)
|
77
|
+
get("/datasets/#{id}")
|
78
|
+
end
|
79
|
+
|
80
|
+
# List datasets
|
81
|
+
# @param limit [Integer] Maximum number of datasets to return
|
82
|
+
# @param offset [Integer] Offset for pagination
|
83
|
+
# @return [Hash] Response with status and body
|
84
|
+
def list_datasets(limit: 100, offset: 0)
|
85
|
+
get("/datasets?limit=#{limit}&offset=#{offset}")
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create an example in a dataset
|
89
|
+
# @param dataset_id [String] Dataset ID
|
90
|
+
# @param inputs [Hash] Example inputs
|
91
|
+
# @param outputs [Hash] Example outputs (optional)
|
92
|
+
# @return [Hash] Response with status and body
|
93
|
+
def create_example(dataset_id, inputs, outputs = nil)
|
94
|
+
payload = {
|
95
|
+
dataset_id: dataset_id,
|
96
|
+
inputs: inputs,
|
97
|
+
outputs: outputs
|
98
|
+
}.compact
|
99
|
+
post("/examples", payload)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Get an example by ID
|
103
|
+
# @param id [String] Example ID
|
104
|
+
# @return [Hash] Response with status and body
|
105
|
+
def get_example(id)
|
106
|
+
get("/examples/#{id}")
|
107
|
+
end
|
108
|
+
|
109
|
+
# List examples in a dataset
|
110
|
+
# @param dataset_id [String] Dataset ID
|
111
|
+
# @param limit [Integer] Maximum number of examples to return
|
112
|
+
# @param offset [Integer] Offset for pagination
|
113
|
+
# @return [Hash] Response with status and body
|
114
|
+
def list_examples(dataset_id, limit: 100, offset: 0)
|
115
|
+
get("/datasets/#{dataset_id}/examples?limit=#{limit}&offset=#{offset}")
|
116
|
+
end
|
117
|
+
|
118
|
+
# Create feedback for a run
|
119
|
+
# @param run_id [String] Run ID
|
120
|
+
# @param key [String] Feedback key
|
121
|
+
# @param score [Float, Integer, Boolean] Feedback score
|
122
|
+
# @param value [Hash] Additional feedback data (optional)
|
123
|
+
# @param comment [String] Feedback comment (optional)
|
124
|
+
# @return [Hash] Response with status and body
|
125
|
+
def create_feedback(run_id, key, score, value: nil, comment: nil)
|
126
|
+
payload = {
|
127
|
+
id: SecureRandom.uuid,
|
128
|
+
run_id: run_id,
|
129
|
+
key: key,
|
130
|
+
score: score,
|
131
|
+
value: value,
|
132
|
+
comment: comment
|
133
|
+
}.compact
|
134
|
+
post("/feedback", payload)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get feedback by ID
|
138
|
+
# @param id [String] Feedback ID
|
139
|
+
# @return [Hash] Response with status and body
|
140
|
+
def get_feedback(id)
|
141
|
+
get("/feedback/#{id}")
|
142
|
+
end
|
143
|
+
|
144
|
+
# List feedback for a run
|
145
|
+
# @param run_id [String] Run ID
|
146
|
+
# @return [Hash] Response with status and body
|
147
|
+
def list_feedback(run_id)
|
148
|
+
get("/runs/#{run_id}/feedback")
|
149
|
+
end
|
150
|
+
|
151
|
+
# Create a project in LangSmith
|
152
|
+
# @param name [String] Project name
|
153
|
+
# @param description [String] Project description (optional)
|
154
|
+
# @return [Hash] Response with status and body
|
155
|
+
def create_project(name, description: nil)
|
156
|
+
payload = {
|
157
|
+
name: name,
|
158
|
+
description: description
|
159
|
+
}.compact
|
160
|
+
post("/projects", payload)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Get a project by name
|
164
|
+
# @param name [String] Project name
|
165
|
+
# @return [Hash] Response with status and body
|
166
|
+
def get_project(name)
|
167
|
+
get("/projects/#{URI.encode_www_form_component(name)}")
|
168
|
+
end
|
169
|
+
|
170
|
+
# List projects
|
171
|
+
# @param limit [Integer] Maximum number of projects to return
|
172
|
+
# @param offset [Integer] Offset for pagination
|
173
|
+
# @return [Hash] Response with status and body
|
174
|
+
def list_projects(limit: 100, offset: 0)
|
175
|
+
get("/projects?limit=#{limit}&offset=#{offset}")
|
176
|
+
end
|
177
|
+
|
178
|
+
# Upload a file attachment
|
179
|
+
# @param file_path [String] Path to the file
|
180
|
+
# @return [Hash] Response with status and body containing the file ID
|
181
|
+
def upload_file(file_path)
|
182
|
+
uri = URI.parse("#{@api_url}/files")
|
183
|
+
|
184
|
+
File.open(file_path, 'rb') do |file|
|
185
|
+
boundary = SecureRandom.hex(16)
|
186
|
+
|
187
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
188
|
+
req['Authorization'] = "Bearer #{@api_key}" if @api_key
|
189
|
+
req['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
190
|
+
|
191
|
+
body = []
|
192
|
+
body << "--#{boundary}\r\n"
|
193
|
+
body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(file_path)}\"\r\n"
|
194
|
+
body << "Content-Type: #{content_type_for_file(file_path)}\r\n\r\n"
|
195
|
+
body << file.read
|
196
|
+
body << "\r\n--#{boundary}--\r\n"
|
197
|
+
|
198
|
+
req.body = body.join
|
199
|
+
|
200
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
201
|
+
http.use_ssl = uri.scheme == "https"
|
202
|
+
http.open_timeout = Config[:open_timeout_seconds]
|
203
|
+
http.read_timeout = Config[:timeout_seconds]
|
204
|
+
|
205
|
+
res = http.request(req)
|
206
|
+
{
|
207
|
+
status: res.code.to_i,
|
208
|
+
body: (JSON.parse(res.body) rescue { "raw" => res.body })
|
209
|
+
}
|
210
|
+
end
|
211
|
+
rescue => e
|
212
|
+
{ status: 0, error: e.message }
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
# Make a GET request
|
218
|
+
# @param path [String] API path
|
219
|
+
# @return [Hash] Response with status and body
|
220
|
+
def get(path)
|
221
|
+
request(Net::HTTP::Get, path, nil)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Make a POST request
|
225
|
+
# @param path [String] API path
|
226
|
+
# @param payload [Hash] Request payload
|
227
|
+
# @return [Hash] Response with status and body
|
228
|
+
def post(path, payload)
|
229
|
+
request(Net::HTTP::Post, path, payload)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Make a PATCH request
|
233
|
+
# @param path [String] API path
|
234
|
+
# @param payload [Hash] Request payload
|
235
|
+
# @return [Hash] Response with status and body
|
236
|
+
def patch(path, payload)
|
237
|
+
request(Net::HTTP::Patch, path, payload)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Make a DELETE request
|
241
|
+
# @param path [String] API path
|
242
|
+
# @return [Hash] Response with status and body
|
243
|
+
def delete(path)
|
244
|
+
request(Net::HTTP::Delete, path, nil)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Make an HTTP request
|
248
|
+
# @param klass [Class] Net::HTTP request class
|
249
|
+
# @param path [String] API path
|
250
|
+
# @param payload [Hash] Request payload
|
251
|
+
# @return [Hash] Response with status and body
|
252
|
+
def request(klass, path, payload)
|
253
|
+
uri = URI.parse("#{@api_url}#{path}")
|
254
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
255
|
+
http.use_ssl = uri.scheme == "https"
|
256
|
+
http.open_timeout = Config[:open_timeout_seconds]
|
257
|
+
http.read_timeout = Config[:timeout_seconds]
|
258
|
+
|
259
|
+
req = klass.new(uri.request_uri)
|
260
|
+
req["Authorization"] = "Bearer #{@api_key}" if @api_key
|
261
|
+
|
262
|
+
if payload
|
263
|
+
req["Content-Type"] = "application/json"
|
264
|
+
req.body = JSON.generate(payload)
|
265
|
+
end
|
266
|
+
|
267
|
+
res = http.request(req)
|
268
|
+
{
|
269
|
+
status: res.code.to_i,
|
270
|
+
body: (JSON.parse(res.body) rescue { "raw" => res.body })
|
271
|
+
}
|
272
|
+
rescue => e
|
273
|
+
{ status: 0, error: e.message }
|
274
|
+
end
|
275
|
+
|
276
|
+
# Determine content type for a file
|
277
|
+
# @param file_path [String] Path to the file
|
278
|
+
# @return [String] Content type
|
279
|
+
def content_type_for_file(file_path)
|
280
|
+
ext = File.extname(file_path).downcase
|
281
|
+
case ext
|
282
|
+
when ".jpg", ".jpeg" then "image/jpeg"
|
283
|
+
when ".png" then "image/png"
|
284
|
+
when ".pdf" then "application/pdf"
|
285
|
+
when ".txt" then "text/plain"
|
286
|
+
when ".json" then "application/json"
|
287
|
+
when ".csv" then "text/csv"
|
288
|
+
else "application/octet-stream"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "ostruct"
|
5
|
+
require "logger"
|
6
|
+
require "singleton"
|
7
|
+
|
8
|
+
module LangsmithrbRails
|
9
|
+
# Configuration class for LangsmithrbRails
|
10
|
+
class Config
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
# Default configuration values
|
14
|
+
DEFAULTS = {
|
15
|
+
api_url: "https://api.smith.langchain.com",
|
16
|
+
project_name: "default",
|
17
|
+
api_key: ENV["LANGSMITH_API_KEY"],
|
18
|
+
sampling: 1.0,
|
19
|
+
redact_by_default: true,
|
20
|
+
timeout_seconds: 3.0,
|
21
|
+
open_timeout_seconds: 1.0,
|
22
|
+
env: ENV["RAILS_ENV"] || "development",
|
23
|
+
enabled: true,
|
24
|
+
# Advanced tracing options
|
25
|
+
trace_all: ENV.fetch("LANGSMITH_TRACE_ALL", "false") == "true",
|
26
|
+
trace_level: ENV.fetch("LANGSMITH_TRACE_LEVEL", "info").to_sym,
|
27
|
+
# OpenTelemetry options
|
28
|
+
otel_enabled: ENV.fetch("LANGSMITH_OTEL_ENABLED", "false") == "true",
|
29
|
+
otel_service_name: ENV.fetch("LANGSMITH_OTEL_SERVICE_NAME", "langsmithrb_rails"),
|
30
|
+
# Evaluation options
|
31
|
+
evaluation_enabled: ENV.fetch("LANGSMITH_EVALUATION_ENABLED", "false") == "true",
|
32
|
+
# Logging options
|
33
|
+
log_level: ENV.fetch("LANGSMITH_LOG_LEVEL", "info").to_sym,
|
34
|
+
log_to_stdout: ENV.fetch("LANGSMITH_LOG_TO_STDOUT", "false") == "true"
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
# Configuration attributes
|
38
|
+
attr_accessor :api_key, :api_url, :project_name, :enabled, :sampling,
|
39
|
+
:trace_all, :trace_level, :otel_enabled, :otel_service_name,
|
40
|
+
:evaluation_enabled, :log_level, :log_to_stdout
|
41
|
+
attr_reader :logger
|
42
|
+
|
43
|
+
# Initialize with default values
|
44
|
+
def initialize
|
45
|
+
# Set defaults first
|
46
|
+
DEFAULTS.each do |key, value|
|
47
|
+
instance_variable_set("@#{key}", value)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Then override with environment variables if present
|
51
|
+
load_from_env
|
52
|
+
|
53
|
+
# Initialize logger
|
54
|
+
@logger = create_logger(@log_level, @log_to_stdout)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Load configuration from environment variables
|
58
|
+
# @return [Config] Self for chaining
|
59
|
+
def load_from_env
|
60
|
+
@api_key = ENV["LANGSMITH_API_KEY"] if ENV["LANGSMITH_API_KEY"]
|
61
|
+
@api_url = ENV["LANGSMITH_API_URL"] if ENV["LANGSMITH_API_URL"]
|
62
|
+
@project_name = ENV["LANGSMITH_PROJECT"] if ENV["LANGSMITH_PROJECT"]
|
63
|
+
@enabled = ENV["LANGSMITH_ENABLED"] == "true" if ENV.key?("LANGSMITH_ENABLED")
|
64
|
+
@sampling = ENV["LANGSMITH_SAMPLING"].to_f if ENV["LANGSMITH_SAMPLING"]
|
65
|
+
@trace_all = ENV["LANGSMITH_TRACE_ALL"] == "true" if ENV.key?("LANGSMITH_TRACE_ALL")
|
66
|
+
@trace_level = ENV["LANGSMITH_TRACE_LEVEL"].to_sym if ENV["LANGSMITH_TRACE_LEVEL"]
|
67
|
+
@otel_enabled = ENV["LANGSMITH_OTEL_ENABLED"] == "true" if ENV.key?("LANGSMITH_OTEL_ENABLED")
|
68
|
+
@otel_service_name = ENV["LANGSMITH_OTEL_SERVICE_NAME"] if ENV["LANGSMITH_OTEL_SERVICE_NAME"]
|
69
|
+
@evaluation_enabled = ENV["LANGSMITH_EVALUATION_ENABLED"] == "true" if ENV.key?("LANGSMITH_EVALUATION_ENABLED")
|
70
|
+
@log_level = ENV["LANGSMITH_LOG_LEVEL"].to_sym if ENV["LANGSMITH_LOG_LEVEL"]
|
71
|
+
@log_to_stdout = ENV["LANGSMITH_LOG_TO_STDOUT"] == "true" if ENV.key?("LANGSMITH_LOG_TO_STDOUT")
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Load configuration from YAML file
|
76
|
+
# @param path [String] Path to YAML file
|
77
|
+
# @return [Config] Self for chaining
|
78
|
+
def load_from_yaml(path)
|
79
|
+
return self unless File.exist?(path)
|
80
|
+
|
81
|
+
yml = YAML.load_file(path)
|
82
|
+
env = (ENV["RAILS_ENV"] || "development").to_s
|
83
|
+
config = yml.fetch(env, {})
|
84
|
+
|
85
|
+
config.each do |key, value|
|
86
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
87
|
+
end
|
88
|
+
|
89
|
+
# Reset logger if log settings changed
|
90
|
+
@logger = create_logger(@log_level, @log_to_stdout)
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check if LangSmith is enabled
|
96
|
+
# @return [Boolean] Whether LangSmith is enabled
|
97
|
+
def enabled?
|
98
|
+
@enabled
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if a trace should be sampled
|
102
|
+
# @return [Boolean] Whether to sample the trace
|
103
|
+
def should_sample?
|
104
|
+
return true if @sampling >= 1.0
|
105
|
+
return false if @sampling <= 0.0
|
106
|
+
Random.rand < @sampling
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check if OpenTelemetry is enabled
|
110
|
+
# @return [Boolean] Whether OpenTelemetry is enabled
|
111
|
+
def otel_enabled?
|
112
|
+
@otel_enabled
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if evaluation is enabled
|
116
|
+
# @return [Boolean] Whether evaluation is enabled
|
117
|
+
def evaluation_enabled?
|
118
|
+
@evaluation_enabled
|
119
|
+
end
|
120
|
+
|
121
|
+
# Set log level
|
122
|
+
# @param level [Symbol, String] Log level
|
123
|
+
# @return [Symbol] Log level
|
124
|
+
def log_level=(level)
|
125
|
+
level = level.to_sym if level.is_a?(String)
|
126
|
+
unless [:debug, :info, :warn, :error, :fatal].include?(level)
|
127
|
+
raise ArgumentError, "Invalid log level: #{level}"
|
128
|
+
end
|
129
|
+
@log_level = level
|
130
|
+
@logger = create_logger(@log_level, @log_to_stdout)
|
131
|
+
@log_level
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get logger instance
|
135
|
+
# @return [Logger] Logger instance
|
136
|
+
def logger
|
137
|
+
@logger ||= create_logger(@log_level, @log_to_stdout)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
# Create a logger
|
143
|
+
# @param level [Symbol] Log level
|
144
|
+
# @param to_stdout [Boolean] Whether to log to STDOUT
|
145
|
+
# @return [Logger] Logger instance
|
146
|
+
def create_logger(level, to_stdout)
|
147
|
+
logger = Logger.new(to_stdout ? STDOUT : IO::NULL)
|
148
|
+
logger.level = parse_log_level(level)
|
149
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
150
|
+
"[LangsmithrbRails] [#{severity}] #{msg}\n"
|
151
|
+
end
|
152
|
+
logger
|
153
|
+
end
|
154
|
+
|
155
|
+
# Parse log level
|
156
|
+
# @param level [Symbol, String] Log level
|
157
|
+
# @return [Integer] Logger level constant
|
158
|
+
def parse_log_level(level)
|
159
|
+
case level.to_s.downcase
|
160
|
+
when "debug" then Logger::DEBUG
|
161
|
+
when "info" then Logger::INFO
|
162
|
+
when "warn" then Logger::WARN
|
163
|
+
when "error" then Logger::ERROR
|
164
|
+
when "fatal" then Logger::FATAL
|
165
|
+
else Logger::INFO
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|