langsmithrb 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.
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Langsmith
6
+ class Dataset
7
+ attr_reader :id, :name, :description, :created_at, :tenant_id, :data
8
+
9
+ # Initialize a new Dataset instance
10
+ #
11
+ # @param client [Langsmith::Client] The LangSmith client
12
+ # @param data [Hash] Dataset data from the API
13
+ def initialize(client, data)
14
+ @client = client
15
+ @id = data[:id] || data["id"]
16
+ @name = data[:name] || data["name"]
17
+ @description = data[:description] || data["description"]
18
+ created_at_value = data[:created_at] || data["created_at"]
19
+ @created_at = created_at_value ? Time.parse(created_at_value) : nil
20
+ @tenant_id = data[:tenant_id] || data["tenant_id"]
21
+ @data = data
22
+ end
23
+
24
+ # Create a new example in this dataset
25
+ #
26
+ # @param inputs [Hash] Input values for the example
27
+ # @param outputs [Hash, nil] Output values for the example (optional)
28
+ # @param metadata [Hash, nil] Additional metadata for the example (optional)
29
+ # @return [Langsmith::Example] The created example
30
+ def create_example(inputs:, outputs: nil, metadata: nil)
31
+ data = {
32
+ dataset_id: @id,
33
+ inputs: inputs
34
+ }
35
+ data[:outputs] = outputs if outputs
36
+ data[:metadata] = metadata if metadata
37
+
38
+ response = @client.post("/examples", data)
39
+ Example.new(@client, response)
40
+ end
41
+
42
+ # Get a specific example by ID
43
+ #
44
+ # @param example_id [String] ID of the example to get
45
+ # @return [Langsmith::Example] The requested example
46
+ def get_example(example_id:)
47
+ response = @client.get("/examples/#{example_id}")
48
+ Example.new(@client, response)
49
+ end
50
+
51
+ # List examples in this dataset
52
+ #
53
+ # @param limit [Integer] Maximum number of examples to return
54
+ # @param offset [Integer] Number of examples to skip
55
+ # @return [Array<Langsmith::Example>] List of examples in this dataset
56
+ def list_examples(limit: 100, offset: 0)
57
+ params = {
58
+ dataset_id: @id,
59
+ limit: limit,
60
+ offset: offset
61
+ }
62
+ response = @client.get("/examples", params)
63
+ response.map { |example_data| Example.new(@client, example_data) }
64
+ end
65
+
66
+ # Create multiple examples in batch
67
+ #
68
+ # @param examples [Array<Hash>] Array of example data, each containing :inputs and optionally :outputs and :metadata
69
+ # @return [Array<Example>] The created examples
70
+ def create_examples_batch(examples:)
71
+ @client.create_examples_batch(dataset_id: @id, examples: examples)
72
+ end
73
+
74
+ # Create a new evaluation run on this dataset
75
+ #
76
+ # @param evaluator_name [String] Name of the evaluator to use
77
+ # @param run_ids [Array<String>] IDs of runs to evaluate
78
+ # @param metadata [Hash, nil] Additional metadata for the evaluation (optional)
79
+ # @return [Langsmith::Evaluation] The created evaluation run
80
+ def create_evaluation_run(evaluator_name:, run_ids:, metadata: nil)
81
+ data = {
82
+ dataset_id: @id,
83
+ evaluator_name: evaluator_name,
84
+ run_ids: run_ids
85
+ }
86
+ data[:metadata] = metadata if metadata
87
+
88
+ response = @client.post("/evaluations", data)
89
+ Evaluation.new(@client, response)
90
+ end
91
+
92
+ # List evaluation runs for this dataset
93
+ #
94
+ # @param limit [Integer] Maximum number of evaluation runs to return
95
+ # @param offset [Integer] Number of evaluation runs to skip
96
+ # @return [Array<Langsmith::Evaluation>] List of evaluation runs for this dataset
97
+ def list_evaluation_runs(limit: 100, offset: 0)
98
+ params = {
99
+ dataset_id: @id,
100
+ limit: limit,
101
+ offset: offset
102
+ }
103
+ response = @client.get("/evaluations", params)
104
+ response.map { |eval_data| Evaluation.new(@client, eval_data) }
105
+ end
106
+
107
+ # Update this dataset
108
+ #
109
+ # @param name [String, nil] New name for the dataset (optional)
110
+ # @param description [String, nil] New description for the dataset (optional)
111
+ # @return [Langsmith::Dataset] The updated dataset
112
+ def update(name: nil, description: nil)
113
+ data = {}
114
+ data[:name] = name if name
115
+ data[:description] = description if description
116
+
117
+ response = @client.patch("/datasets/#{@id}", data)
118
+ @name = response[:name] || response["name"] if name
119
+ @description = response[:description] || response["description"] if description
120
+ @data = response
121
+ self
122
+ end
123
+
124
+ # Delete this dataset
125
+ #
126
+ # @return [Boolean] True if successful
127
+ def delete
128
+ @client.delete("/datasets/#{@id}")
129
+ true
130
+ end
131
+ end
132
+
133
+ # Example class to represent dataset examples
134
+ class Example
135
+ attr_reader :id, :dataset_id, :inputs, :outputs, :metadata, :created_at
136
+
137
+ def initialize(client, data)
138
+ @client = client
139
+ @id = data[:id] || data["id"]
140
+ @dataset_id = data[:dataset_id] || data["dataset_id"]
141
+ @inputs = data[:inputs] || data["inputs"]
142
+ @outputs = data[:outputs] || data["outputs"]
143
+ @metadata = data[:metadata] || data["metadata"]
144
+ created_at_value = data[:created_at] || data["created_at"]
145
+ @created_at = created_at_value ? Time.parse(created_at_value) : nil
146
+ @data = data
147
+ end
148
+
149
+ # Update this example
150
+ #
151
+ # @param inputs [Hash, nil] New input values (optional)
152
+ # @param outputs [Hash, nil] New output values (optional)
153
+ # @param metadata [Hash, nil] New metadata (optional)
154
+ # @return [Langsmith::Example] The updated example
155
+ def update(inputs: nil, outputs: nil, metadata: nil)
156
+ data = {}
157
+ data[:inputs] = inputs if inputs
158
+ data[:outputs] = outputs if outputs
159
+ data[:metadata] = metadata if metadata
160
+
161
+ response = @client.patch("/examples/#{@id}", data)
162
+ @inputs = response[:inputs] || response["inputs"] if inputs
163
+ @outputs = response[:outputs] || response["outputs"] if outputs
164
+ @metadata = response[:metadata] || response["metadata"] if metadata
165
+ @data = response
166
+ self
167
+ end
168
+
169
+ # Delete this example
170
+ #
171
+ # @return [Boolean] True if successful
172
+ def delete
173
+ @client.delete("/examples/#{@id}")
174
+ true
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Langsmith
6
+ class Evaluation
7
+ attr_reader :id, :dataset_id, :evaluator_name, :status, :created_at, :run_ids, :metadata, :data
8
+
9
+ # Initialize a new Evaluation instance
10
+ #
11
+ # @param client [Langsmith::Client] The LangSmith client
12
+ # @param data [Hash] Evaluation data from the API
13
+ def initialize(client, data)
14
+ @client = client
15
+ @id = data[:id] || data["id"]
16
+ @dataset_id = data[:dataset_id] || data["dataset_id"]
17
+ @evaluator_name = data[:evaluator_name] || data["evaluator_name"]
18
+ @status = data[:status] || data["status"]
19
+ created_at_value = data[:created_at] || data["created_at"]
20
+ @created_at = created_at_value ? Time.parse(created_at_value) : nil
21
+ @run_ids = data[:run_ids] || data["run_ids"] || []
22
+ @metadata = data[:metadata] || data["metadata"] || {}
23
+ @data = data
24
+ end
25
+
26
+ # Get the results of this evaluation
27
+ #
28
+ # @param limit [Integer] Maximum number of results to return
29
+ # @param offset [Integer] Number of results to skip
30
+ # @return [Array<Hash>] Evaluation results
31
+ def results(limit: 100, offset: 0)
32
+ params = {
33
+ limit: limit,
34
+ offset: offset
35
+ }
36
+ @client.get("/evaluations/#{@id}/results", params)
37
+ end
38
+
39
+ # Get the status of this evaluation
40
+ #
41
+ # @return [String] Current status of the evaluation
42
+ def refresh_status
43
+ response = @client.get("/evaluations/#{@id}")
44
+ @status = response[:status] || response["status"]
45
+ @data = response
46
+ @status
47
+ end
48
+
49
+ # Check if the evaluation is completed
50
+ #
51
+ # @return [Boolean] True if the evaluation is completed
52
+ def completed?
53
+ refresh_status == "complete"
54
+ end
55
+
56
+ # Wait for the evaluation to complete
57
+ #
58
+ # @param timeout [Integer] Maximum time to wait in seconds
59
+ # @param interval [Integer] Time between status checks in seconds
60
+ # @return [Boolean] True if the evaluation completed within the timeout
61
+ def wait_for_completion(timeout: 300, interval: 5)
62
+ start_time = Time.now
63
+
64
+ while Time.now - start_time < timeout
65
+ return true if completed?
66
+ sleep(interval)
67
+ end
68
+
69
+ false
70
+ end
71
+
72
+ # Update this evaluation's metadata
73
+ #
74
+ # @param metadata [Hash] New metadata for the evaluation
75
+ # @return [Langsmith::Evaluation] The updated evaluation
76
+ def update_metadata(metadata:)
77
+ data = { metadata: metadata }
78
+ response = @client.patch("/evaluations/#{@id}", data)
79
+ @metadata = response[:metadata] || response["metadata"]
80
+ @data = response
81
+ self
82
+ end
83
+
84
+ # Cancel this evaluation
85
+ #
86
+ # @return [Boolean] True if successful
87
+ def cancel
88
+ @client.post("/evaluations/#{@id}/cancel")
89
+ refresh_status
90
+ true
91
+ end
92
+
93
+ # Delete this evaluation
94
+ #
95
+ # @return [Boolean] True if successful
96
+ def delete
97
+ @client.delete("/evaluations/#{@id}")
98
+ true
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ class Feedback
5
+ attr_reader :id, :run_id, :key, :score, :comment, :created_at
6
+
7
+ # Initialize a new Feedback instance
8
+ #
9
+ # @param client [Langsmith::Client] The LangSmith client
10
+ # @param data [Hash] Feedback data from the API
11
+ def initialize(client, data)
12
+ @client = client
13
+ @id = data["id"]
14
+ @run_id = data["run_id"]
15
+ @key = data["key"]
16
+ @score = data["score"]
17
+ @comment = data["comment"]
18
+ @created_at = data["created_at"] ? Time.parse(data["created_at"]) : nil
19
+ end
20
+
21
+ # Update this feedback
22
+ #
23
+ # @param score [Float] New feedback score
24
+ # @param comment [String] New feedback comment
25
+ # @return [Langsmith::Feedback] The updated feedback
26
+ def update(score: nil, comment: nil)
27
+ data = {}
28
+ data[:score] = score if score
29
+ data[:comment] = comment if comment
30
+
31
+ response = @client.patch("/feedback/#{@id}", data)
32
+ Langsmith::Feedback.new(@client, response)
33
+ end
34
+
35
+ # Delete this feedback
36
+ #
37
+ # @return [Boolean] True if the feedback was deleted successfully
38
+ def delete
39
+ response = @client.delete("/feedback/#{@id}")
40
+ response["success"] == true
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ class Project
5
+ attr_reader :id, :name, :description, :created_at, :tenant_id
6
+
7
+ # Initialize a new Project instance
8
+ #
9
+ # @param client [Langsmith::Client] The LangSmith client
10
+ # @param data [Hash] Project data from the API
11
+ def initialize(client, data)
12
+ @client = client
13
+ @id = data["id"]
14
+ @name = data["name"]
15
+ @description = data["description"]
16
+ @created_at = data["created_at"] ? Time.parse(data["created_at"]) : nil
17
+ @tenant_id = data["tenant_id"]
18
+ end
19
+
20
+ # Create a new run in this project
21
+ #
22
+ # @param name [String] Name of the run
23
+ # @param run_type [String] Type of run (e.g., llm, chain, tool)
24
+ # @param inputs [Hash] Input values for the run
25
+ # @param extra [Hash] Additional metadata for the run
26
+ # @return [Langsmith::Run] The created run
27
+ def create_run(name:, run_type:, inputs: {}, extra: {})
28
+ @client.create_run(name: name, run_type: run_type, project_name: @name, inputs: inputs, extra: extra)
29
+ end
30
+
31
+ # List runs in this project
32
+ #
33
+ # @param run_type [String] Filter by run type
34
+ # @param limit [Integer] Maximum number of runs to return
35
+ # @return [Array<Langsmith::Run>] List of runs in this project
36
+ def list_runs(run_type: nil, limit: 100)
37
+ @client.list_runs(project_name: @name, run_type: run_type, limit: limit)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Langsmith
6
+ class Run
7
+ attr_reader :id, :name, :run_type, :start_time, :end_time, :status
8
+ attr_reader :inputs, :outputs, :error, :project_name, :trace_id, :parent_run_id
9
+
10
+ # Initialize a new Run instance
11
+ #
12
+ # @param client [Langsmith::Client] The LangSmith client
13
+ # @param data [Hash] Run data from the API
14
+ def initialize(client, data)
15
+ @client = client
16
+ @id = data["id"]
17
+ @name = data["name"]
18
+ @run_type = data["run_type"]
19
+ @start_time = data["start_time"] ? Time.parse(data["start_time"]) : Time.now
20
+ @end_time = data["end_time"] ? Time.parse(data["end_time"]) : nil
21
+ @status = data["status"] || "in_progress"
22
+ @inputs = data["inputs"] || {}
23
+ @outputs = data["outputs"] || {}
24
+ @error = data["error"]
25
+ @project_name = data["project_name"]
26
+ @trace_id = data["trace_id"]
27
+ @parent_run_id = data["parent_run_id"]
28
+ @extra = data["extra"] || {}
29
+ end
30
+
31
+ # Update the run with outputs and mark it as completed
32
+ #
33
+ # @param outputs [Hash] Output values from the run
34
+ # @return [Langsmith::Run] The updated run
35
+ def end(outputs: nil, error: nil)
36
+ end_time = Time.now
37
+
38
+ if error
39
+ @client.update_run(run_id: @id, end_time: end_time, error: error)
40
+ else
41
+ @client.update_run(run_id: @id, outputs: outputs, end_time: end_time)
42
+ end
43
+ end
44
+
45
+ # Create a child run for this run
46
+ #
47
+ # @param name [String] Name of the child run
48
+ # @param run_type [String] Type of the child run
49
+ # @param inputs [Hash] Input values for the child run
50
+ # @param extra [Hash] Additional metadata for the child run
51
+ # @return [Langsmith::Run] The created child run
52
+ def create_child_run(name:, run_type:, inputs: {}, extra: {})
53
+ child_extra = extra.merge(parent_run_id: @id, trace_id: @trace_id)
54
+
55
+ data = {
56
+ name: name,
57
+ run_type: run_type,
58
+ inputs: inputs,
59
+ extra: child_extra
60
+ }
61
+ data[:project_name] = @project_name if @project_name
62
+
63
+ response = @client.post("/runs", data)
64
+ Langsmith::Run.new(@client, response)
65
+ end
66
+
67
+ # Add feedback to this run
68
+ #
69
+ # @param key [String] Feedback key (e.g., "correctness", "helpfulness")
70
+ # @param score [Float] Feedback score (typically 0.0 to 1.0)
71
+ # @param comment [String] Optional comment with the feedback
72
+ # @return [Langsmith::Feedback] The created feedback
73
+ def add_feedback(key:, score:, comment: nil)
74
+ @client.create_feedback(run_id: @id, key: key, score: score, comment: comment)
75
+ end
76
+
77
+ # Get all feedback for this run
78
+ #
79
+ # @return [Array<Langsmith::Feedback>] List of feedback for this run
80
+ def get_feedback
81
+ @client.get_feedback(run_id: @id)
82
+ end
83
+
84
+ # Get metadata from the run
85
+ #
86
+ # @param key [String] The metadata key to retrieve
87
+ # @return [Object] The metadata value
88
+ def get_metadata(key)
89
+ @extra[key]
90
+ end
91
+
92
+ # Check if the run is completed
93
+ #
94
+ # @return [Boolean] True if the run is completed
95
+ def completed?
96
+ !@end_time.nil?
97
+ end
98
+
99
+ # Check if the run has an error
100
+ #
101
+ # @return [Boolean] True if the run has an error
102
+ def error?
103
+ !@error.nil?
104
+ end
105
+
106
+ # Get the duration of the run in seconds
107
+ #
108
+ # @return [Float] Duration in seconds, or nil if the run is not completed
109
+ def duration
110
+ return nil unless @end_time
111
+ @end_time - @start_time
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ class Trace
5
+ attr_reader :id, :name, :start_time, :end_time, :runs
6
+
7
+ # Initialize a new Trace instance
8
+ #
9
+ # @param client [Langsmith::Client] The LangSmith client
10
+ # @param trace_id [String] ID of the trace
11
+ def initialize(client, trace_id)
12
+ @client = client
13
+ @id = trace_id
14
+ @runs = []
15
+ refresh
16
+ end
17
+
18
+ # Refresh the trace data from the API
19
+ #
20
+ # @return [Langsmith::Trace] The updated trace
21
+ def refresh
22
+ response = @client.get("/traces/#{@id}")
23
+ @name = response["name"]
24
+ @start_time = response["start_time"] ? Time.parse(response["start_time"]) : nil
25
+ @end_time = response["end_time"] ? Time.parse(response["end_time"]) : nil
26
+
27
+ # Get all runs associated with this trace
28
+ runs_response = @client.get("/runs", { trace_id: @id })
29
+ @runs = runs_response.map { |run_data| Langsmith::Run.new(@client, run_data) }
30
+
31
+ self
32
+ end
33
+
34
+ # Get the root run of this trace
35
+ #
36
+ # @return [Langsmith::Run] The root run
37
+ def root_run
38
+ @runs.find { |run| run.parent_run_id.nil? }
39
+ end
40
+
41
+ # Get child runs of a specific run
42
+ #
43
+ # @param parent_run_id [String] ID of the parent run
44
+ # @return [Array<Langsmith::Run>] Child runs
45
+ def child_runs(parent_run_id)
46
+ @runs.select { |run| run.parent_run_id == parent_run_id }
47
+ end
48
+
49
+ # Check if the trace is completed
50
+ #
51
+ # @return [Boolean] True if the trace is completed
52
+ def completed?
53
+ !@end_time.nil?
54
+ end
55
+
56
+ # Get the duration of the trace in seconds
57
+ #
58
+ # @return [Float] Duration in seconds, or nil if the trace is not completed
59
+ def duration
60
+ return nil unless @end_time && @start_time
61
+ @end_time - @start_time
62
+ end
63
+
64
+ # Get a hierarchical representation of the trace
65
+ #
66
+ # @return [Hash] Hierarchical representation of the trace
67
+ def to_hierarchy
68
+ root = root_run
69
+ return {} unless root
70
+
71
+ build_hierarchy(root)
72
+ end
73
+
74
+ private
75
+
76
+ def build_hierarchy(run)
77
+ children = child_runs(run.id)
78
+
79
+ result = {
80
+ id: run.id,
81
+ name: run.name,
82
+ run_type: run.run_type,
83
+ start_time: run.start_time,
84
+ end_time: run.end_time,
85
+ status: run.status,
86
+ inputs: run.inputs,
87
+ outputs: run.outputs,
88
+ error: run.error
89
+ }
90
+
91
+ result[:children] = children.map { |child| build_hierarchy(child) } unless children.empty?
92
+
93
+ result
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ VERSION = "0.1.0"
5
+ Version = VERSION
6
+ end
data/lib/langsmith.rb ADDED
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "langsmith/version"
5
+
6
+ module Langsmith
7
+ class << self
8
+ # @return [Logger]
9
+ attr_accessor :logger
10
+ # @return [Pathname]
11
+ attr_reader :root
12
+ # @return [String]
13
+ attr_accessor :api_key
14
+ # @return [String]
15
+ attr_accessor :api_url
16
+ end
17
+
18
+ module Errors
19
+ class BaseError < StandardError; end
20
+ class AuthenticationError < BaseError; end
21
+ class APIError < BaseError; end
22
+ class ResourceNotFoundError < BaseError; end
23
+ end
24
+
25
+ module Colorizer
26
+ class << self
27
+ def red(str)
28
+ "\e[31m#{str}\e[0m"
29
+ end
30
+
31
+ def green(str)
32
+ "\e[32m#{str}\e[0m"
33
+ end
34
+
35
+ def yellow(str)
36
+ "\e[33m#{str}\e[0m"
37
+ end
38
+
39
+ def blue(str)
40
+ "\e[34m#{str}\e[0m"
41
+ end
42
+
43
+ def colorize_logger_msg(msg, severity)
44
+ return msg unless msg.is_a?(String)
45
+
46
+ return red(msg) if severity.to_sym == :ERROR
47
+ return yellow(msg) if severity.to_sym == :WARN
48
+ msg
49
+ end
50
+ end
51
+ end
52
+
53
+ LOGGER_OPTIONS = {
54
+ progname: "Langsmith.rb",
55
+
56
+ formatter: ->(severity, time, progname, msg) do
57
+ Logger::Formatter.new.call(
58
+ severity,
59
+ time,
60
+ "[#{progname}]",
61
+ Colorizer.colorize_logger_msg(msg, severity)
62
+ )
63
+ end
64
+ }.freeze
65
+
66
+ # Default API URL for LangSmith
67
+ DEFAULT_API_URL = "https://api.smith.langchain.com".freeze
68
+
69
+ # Set default logger
70
+ self.logger ||= ::Logger.new($stdout, **LOGGER_OPTIONS)
71
+
72
+ # Set root path
73
+ @root = Pathname.new(__dir__)
74
+
75
+ # Set default API URL
76
+ @api_url = DEFAULT_API_URL
77
+ end
78
+
79
+ # Load the Langsmith components
80
+ require "langsmith/client"
81
+ require "langsmith/run"
82
+ require "langsmith/dataset"
83
+ require "langsmith/evaluation"
84
+ require "langsmith/feedback"
85
+ require "langsmith/project"
86
+ require "langsmith/trace"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is an alias for langsmith.rb to make requiring the gem easier
4
+ require "langsmith"