lf-cli 1.0.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,46 @@
1
+ require 'csv'
2
+ require 'json'
3
+
4
+ module Langfuse
5
+ module CLI
6
+ module Formatters
7
+ class CSVFormatter
8
+ def self.format(data)
9
+ return "No data to display" if data.nil? || (data.is_a?(Array) && data.empty?)
10
+
11
+ # Convert single hash to array for consistent handling
12
+ data = [data] unless data.is_a?(Array)
13
+
14
+ # Get all unique keys from all rows
15
+ headers = data.flat_map(&:keys).uniq
16
+
17
+ # Generate CSV
18
+ CSV.generate do |csv|
19
+ # Add headers
20
+ csv << headers
21
+
22
+ # Add data rows
23
+ data.each do |row|
24
+ csv << headers.map { |header| format_value(row[header]) }
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def self.format_value(value)
32
+ case value
33
+ when nil
34
+ ''
35
+ when Hash, Array
36
+ value.to_json
37
+ when Time, DateTime
38
+ value.iso8601
39
+ else
40
+ value.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Formatters
6
+ class MarkdownFormatter
7
+ def self.format(data)
8
+ return "No data to display" if data.nil? || (data.is_a?(Array) && data.empty?)
9
+
10
+ # Convert single hash to array for consistent handling
11
+ data = [data] unless data.is_a?(Array)
12
+
13
+ # Get all unique keys from all rows
14
+ headers = data.flat_map(&:keys).uniq
15
+
16
+ # Build markdown table
17
+ output = []
18
+
19
+ # Header row
20
+ output << "| #{headers.join(' | ')} |"
21
+
22
+ # Separator row
23
+ output << "| #{headers.map { '---' }.join(' | ')} |"
24
+
25
+ # Data rows
26
+ data.each do |row|
27
+ values = headers.map { |header| escape_pipes(format_value(row[header])) }
28
+ output << "| #{values.join(' | ')} |"
29
+ end
30
+
31
+ output.join("\n")
32
+ end
33
+
34
+ private
35
+
36
+ def self.format_value(value)
37
+ case value
38
+ when nil
39
+ ''
40
+ when Hash, Array
41
+ value.to_json
42
+ when Time, DateTime
43
+ value.iso8601
44
+ else
45
+ value.to_s
46
+ end
47
+ end
48
+
49
+ def self.escape_pipes(str)
50
+ # Escape pipe characters for markdown tables
51
+ str.gsub('|', '\\|')
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,43 @@
1
+ require 'terminal-table'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Formatters
6
+ class TableFormatter
7
+ def self.format(data)
8
+ return "No data to display" if data.nil? || (data.is_a?(Array) && data.empty?)
9
+
10
+ # Convert single hash to array for consistent handling
11
+ data = [data] unless data.is_a?(Array)
12
+
13
+ # Get all unique keys from all rows
14
+ headers = data.flat_map(&:keys).uniq
15
+
16
+ # Build rows
17
+ rows = data.map do |row|
18
+ headers.map { |header| format_value(row[header]) }
19
+ end
20
+
21
+ # Create table
22
+ table = Terminal::Table.new(headings: headers, rows: rows)
23
+ table.to_s
24
+ end
25
+
26
+ private
27
+
28
+ def self.format_value(value)
29
+ case value
30
+ when nil
31
+ ''
32
+ when Hash, Array
33
+ value.to_json
34
+ when Time, DateTime
35
+ value.iso8601
36
+ else
37
+ value.to_s
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,98 @@
1
+ require 'sorbet-runtime'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Types
6
+ # Enum for metrics view types
7
+ class MetricsView < T::Enum
8
+ enums do
9
+ Traces = new('traces')
10
+ Observations = new('observations')
11
+ ScoresNumeric = new('scores-numeric')
12
+ ScoresCategorical = new('scores-categorical')
13
+ end
14
+ end
15
+
16
+ # Enum for measure types
17
+ class Measure < T::Enum
18
+ enums do
19
+ Count = new('count')
20
+ Latency = new('latency')
21
+ Value = new('value')
22
+ Tokens = new('tokens')
23
+ Cost = new('cost')
24
+ end
25
+ end
26
+
27
+ # Enum for aggregation functions
28
+ class Aggregation < T::Enum
29
+ enums do
30
+ Count = new('count')
31
+ Sum = new('sum')
32
+ Avg = new('avg')
33
+ P50 = new('p50')
34
+ P95 = new('p95')
35
+ P99 = new('p99')
36
+ Min = new('min')
37
+ Max = new('max')
38
+ Histogram = new('histogram')
39
+ end
40
+ end
41
+
42
+ # Enum for time granularity
43
+ class TimeGranularity < T::Enum
44
+ enums do
45
+ Minute = new('minute')
46
+ Hour = new('hour')
47
+ Day = new('day')
48
+ Week = new('week')
49
+ Month = new('month')
50
+ Auto = new('auto')
51
+ end
52
+ end
53
+
54
+ # Enum for observation types
55
+ class ObservationType < T::Enum
56
+ enums do
57
+ Generation = new('generation')
58
+ Span = new('span')
59
+ Event = new('event')
60
+ end
61
+ end
62
+
63
+ # Enum for output formats
64
+ class OutputFormat < T::Enum
65
+ enums do
66
+ Table = new('table')
67
+ JSON = new('json')
68
+ CSV = new('csv')
69
+ Markdown = new('markdown')
70
+ end
71
+ end
72
+
73
+ # Struct for metrics query parameters
74
+ class MetricsQuery < T::Struct
75
+ const :view, String
76
+ const :measure, String
77
+ const :aggregation, String
78
+ prop :dimensions, T.nilable(T::Array[String]), default: nil
79
+ prop :from_timestamp, T.nilable(String), default: nil
80
+ prop :to_timestamp, T.nilable(String), default: nil
81
+ prop :granularity, T.nilable(String), default: nil
82
+ prop :limit, T.nilable(Integer), default: 100
83
+
84
+ def to_h
85
+ {
86
+ 'view' => view,
87
+ 'metrics' => [{ 'measure' => measure, 'aggregation' => aggregation }],
88
+ 'dimensions' => dimensions&.map { |d| { 'field' => d } },
89
+ 'fromTimestamp' => from_timestamp,
90
+ 'toTimestamp' => to_timestamp,
91
+ 'timeDimension' => granularity ? { 'granularity' => granularity } : nil,
92
+ 'limit' => limit
93
+ }.compact
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ module Langfuse
2
+ module CLI
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,142 @@
1
+ require 'thor'
2
+ require_relative 'cli/version'
3
+ require_relative 'cli/config'
4
+ require_relative 'cli/client'
5
+ require_relative 'cli/commands/traces'
6
+ require_relative 'cli/commands/sessions'
7
+ require_relative 'cli/commands/observations'
8
+ require_relative 'cli/commands/scores'
9
+ require_relative 'cli/commands/metrics'
10
+ require_relative 'cli/commands/config'
11
+
12
+ module Langfuse
13
+ module CLI
14
+
15
+ class Main < Thor
16
+ class << self
17
+ def exit_on_failure?
18
+ true
19
+ end
20
+ end
21
+
22
+ # Global options that apply to all commands
23
+ class_option :profile,
24
+ type: :string,
25
+ aliases: '-P',
26
+ desc: 'Config profile to use'
27
+ class_option :format,
28
+ type: :string,
29
+ aliases: '-f',
30
+ enum: %w[table json csv markdown],
31
+ default: 'table',
32
+ desc: 'Output format'
33
+ class_option :output,
34
+ type: :string,
35
+ aliases: '-o',
36
+ desc: 'Output file path (defaults to stdout)'
37
+ class_option :limit,
38
+ type: :numeric,
39
+ aliases: '-l',
40
+ desc: 'Limit number of results'
41
+ class_option :page,
42
+ type: :numeric,
43
+ aliases: '-p',
44
+ desc: 'Page number for pagination'
45
+ class_option :host,
46
+ type: :string,
47
+ desc: 'Langfuse host URL'
48
+ class_option :public_key,
49
+ type: :string,
50
+ desc: 'Langfuse public key'
51
+ class_option :secret_key,
52
+ type: :string,
53
+ desc: 'Langfuse secret key'
54
+ class_option :verbose,
55
+ type: :boolean,
56
+ aliases: '-v',
57
+ default: false,
58
+ desc: 'Verbose output'
59
+ class_option :no_color,
60
+ type: :boolean,
61
+ default: false,
62
+ desc: 'Disable colored output'
63
+
64
+ desc 'version', 'Show version'
65
+ def version
66
+ puts "lf-cli version #{Langfuse::CLI::VERSION}"
67
+ end
68
+
69
+ desc 'traces SUBCOMMAND ...ARGS', 'Manage traces'
70
+ subcommand 'traces', Commands::Traces
71
+
72
+ desc 'sessions SUBCOMMAND ...ARGS', 'Manage sessions'
73
+ subcommand 'sessions', Commands::Sessions
74
+
75
+ desc 'observations SUBCOMMAND ...ARGS', 'Manage observations'
76
+ subcommand 'observations', Commands::Observations
77
+
78
+ desc 'metrics SUBCOMMAND ...ARGS', 'Query metrics'
79
+ subcommand 'metrics', Commands::Metrics
80
+
81
+ desc 'scores SUBCOMMAND ...ARGS', 'Manage scores'
82
+ subcommand 'scores', Commands::Scores
83
+
84
+ desc 'config SUBCOMMAND ...ARGS', 'Manage configuration'
85
+ subcommand 'config', Commands::ConfigCommand
86
+
87
+ private
88
+
89
+ def client
90
+ @client ||= begin
91
+ config = load_config
92
+ unless config.valid?
93
+ error_message = "Missing required configuration: #{config.missing_fields.join(', ')}"
94
+ error_message += "\n\nPlease set environment variables or run: langfuse config setup"
95
+ raise Error, error_message
96
+ end
97
+ Client.new(config)
98
+ end
99
+ end
100
+
101
+ def load_config
102
+ Config.new(
103
+ profile: options[:profile],
104
+ public_key: options[:public_key],
105
+ secret_key: options[:secret_key],
106
+ host: options[:host],
107
+ format: options[:format],
108
+ limit: options[:limit]
109
+ )
110
+ end
111
+
112
+ def output_result(data)
113
+ formatted = format_output(data)
114
+
115
+ if options[:output]
116
+ File.write(options[:output], formatted)
117
+ puts "Output written to #{options[:output]}" if options[:verbose]
118
+ else
119
+ puts formatted
120
+ end
121
+ end
122
+
123
+ def format_output(data)
124
+ case options[:format]
125
+ when 'json'
126
+ require 'json'
127
+ JSON.pretty_generate(data)
128
+ when 'csv'
129
+ # CSV formatting will be implemented in formatters
130
+ require_relative 'cli/formatters/csv_formatter'
131
+ Formatters::CSVFormatter.format(data)
132
+ when 'markdown'
133
+ require_relative 'cli/formatters/markdown_formatter'
134
+ Formatters::MarkdownFormatter.format(data)
135
+ else # table
136
+ require_relative 'cli/formatters/table_formatter'
137
+ Formatters::TableFormatter.format(data)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,10 @@
1
+ require "langfuse/cli/version"
2
+ require "langfuse/cli/config"
3
+ require "langfuse/cli/client"
4
+ require "langfuse/cli"
5
+
6
+ module Langfuse
7
+ module CLI
8
+ class Error < StandardError; end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,303 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lf-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Vicente Reig Rincon de Arellano
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-10-23 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday-retry
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: terminal-table
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: tty-prompt
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.23'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.23'
82
+ - !ruby/object:Gem::Dependency
83
+ name: tty-table
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.12'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.12'
96
+ - !ruby/object:Gem::Dependency
97
+ name: tty-spinner
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.9'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.9'
110
+ - !ruby/object:Gem::Dependency
111
+ name: pastel
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.8'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.8'
124
+ - !ruby/object:Gem::Dependency
125
+ name: chronic
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.10'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.10'
138
+ - !ruby/object:Gem::Dependency
139
+ name: csv
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '3.0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: sorbet-runtime
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '0.5'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '0.5'
166
+ - !ruby/object:Gem::Dependency
167
+ name: rspec
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '3.12'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '3.12'
180
+ - !ruby/object:Gem::Dependency
181
+ name: vcr
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '6.2'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '6.2'
194
+ - !ruby/object:Gem::Dependency
195
+ name: webmock
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - "~>"
199
+ - !ruby/object:Gem::Version
200
+ version: '3.18'
201
+ type: :development
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - "~>"
206
+ - !ruby/object:Gem::Version
207
+ version: '3.18'
208
+ - !ruby/object:Gem::Dependency
209
+ name: byebug
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: '11.1'
215
+ type: :development
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: '11.1'
222
+ - !ruby/object:Gem::Dependency
223
+ name: rake
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - "~>"
227
+ - !ruby/object:Gem::Version
228
+ version: '13.0'
229
+ type: :development
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - "~>"
234
+ - !ruby/object:Gem::Version
235
+ version: '13.0'
236
+ - !ruby/object:Gem::Dependency
237
+ name: rubocop
238
+ requirement: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - "~>"
241
+ - !ruby/object:Gem::Version
242
+ version: '1.50'
243
+ type: :development
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - "~>"
248
+ - !ruby/object:Gem::Version
249
+ version: '1.50'
250
+ description: Unofficial command-line interface for querying and analyzing Langfuse
251
+ LLM observability data. This is a community project not affiliated with Langfuse
252
+ GmbH.
253
+ email:
254
+ - hey@vicente.services
255
+ executables:
256
+ - lf
257
+ extensions: []
258
+ extra_rdoc_files: []
259
+ files:
260
+ - CHANGELOG.md
261
+ - LICENSE
262
+ - README.md
263
+ - bin/lf
264
+ - lib/langfuse/cli.rb
265
+ - lib/langfuse/cli/client.rb
266
+ - lib/langfuse/cli/commands/config.rb
267
+ - lib/langfuse/cli/commands/metrics.rb
268
+ - lib/langfuse/cli/commands/observations.rb
269
+ - lib/langfuse/cli/commands/scores.rb
270
+ - lib/langfuse/cli/commands/sessions.rb
271
+ - lib/langfuse/cli/commands/traces.rb
272
+ - lib/langfuse/cli/config.rb
273
+ - lib/langfuse/cli/formatters/csv_formatter.rb
274
+ - lib/langfuse/cli/formatters/markdown_formatter.rb
275
+ - lib/langfuse/cli/formatters/table_formatter.rb
276
+ - lib/langfuse/cli/types.rb
277
+ - lib/langfuse/cli/version.rb
278
+ - lib/langfuse_cli.rb
279
+ homepage: https://github.com/vicentereig/lf-cli
280
+ licenses:
281
+ - MIT
282
+ metadata:
283
+ homepage_uri: https://github.com/vicentereig/lf-cli
284
+ source_code_uri: https://github.com/vicentereig/lf-cli
285
+ changelog_uri: https://github.com/vicentereig/lf-cli/blob/main/CHANGELOG.md
286
+ rdoc_options: []
287
+ require_paths:
288
+ - lib
289
+ required_ruby_version: !ruby/object:Gem::Requirement
290
+ requirements:
291
+ - - ">="
292
+ - !ruby/object:Gem::Version
293
+ version: 2.7.0
294
+ required_rubygems_version: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - ">="
297
+ - !ruby/object:Gem::Version
298
+ version: '0'
299
+ requirements: []
300
+ rubygems_version: 3.6.5
301
+ specification_version: 4
302
+ summary: An open-source CLI for Langfuse®
303
+ test_files: []