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,161 @@
1
+ require 'thor'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Commands
6
+ class Observations < Thor
7
+ namespace :observations
8
+
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc 'list', 'List observations'
14
+ long_desc <<-LONGDESC
15
+ List observations with optional filtering.
16
+
17
+ Observations represent LLM calls, spans, or events in your traces.
18
+
19
+ FILTERS:
20
+ --type: Observation type
21
+ Valid values: generation, span, event
22
+
23
+ --trace-id: Filter by parent trace ID
24
+
25
+ --name: Filter by observation name
26
+
27
+ --user-id: Filter by user ID
28
+
29
+ --from, --to: Time range (ISO 8601 or relative like "1 hour ago")
30
+
31
+ EXAMPLES:
32
+
33
+ # List all generations
34
+ langfuse observations list --type generation
35
+
36
+ # List observations for a specific trace
37
+ langfuse observations list --trace-id trace_123
38
+
39
+ # List recent observations
40
+ langfuse observations list --from "1 hour ago" --limit 20
41
+
42
+ API REFERENCE:
43
+ Full API documentation: https://api.reference.langfuse.com/
44
+ LONGDESC
45
+ option :trace_id, type: :string, desc: 'Filter by trace ID'
46
+ option :name, type: :string, desc: 'Filter by observation name'
47
+ option :type, type: :string,
48
+ enum: %w[generation span event],
49
+ desc: 'Filter by type'
50
+ option :user_id, type: :string, desc: 'Filter by user ID'
51
+ option :from, type: :string, desc: 'Start timestamp (ISO 8601 or relative)'
52
+ option :to, type: :string, desc: 'End timestamp (ISO 8601 or relative)'
53
+ option :limit, type: :numeric, desc: 'Limit number of results'
54
+ option :page, type: :numeric, desc: 'Page number'
55
+ def list
56
+ filters = build_filters(options)
57
+ observations = client.list_observations(filters)
58
+ output_result(observations)
59
+ rescue Client::AuthenticationError => e
60
+ puts "Authentication Error: #{e.message}"
61
+ exit 1
62
+ rescue Client::APIError => e
63
+ puts "Error: #{e.message}"
64
+ exit 1
65
+ end
66
+
67
+ desc 'get OBSERVATION_ID', 'Get a specific observation'
68
+ def get(observation_id)
69
+ observation = client.get_observation(observation_id)
70
+ output_result(observation)
71
+ rescue Client::NotFoundError => e
72
+ puts "Error: Observation not found - #{observation_id}"
73
+ exit 1
74
+ rescue Client::APIError => e
75
+ puts "Error: #{e.message}"
76
+ exit 1
77
+ end
78
+
79
+ private
80
+
81
+ def client
82
+ @client ||= begin
83
+ config = load_config
84
+ unless config.valid?
85
+ error_message = "Missing required configuration: #{config.missing_fields.join(', ')}"
86
+ error_message += "\n\nPlease set environment variables or run: langfuse config setup"
87
+ raise Error, error_message
88
+ end
89
+ Client.new(config)
90
+ end
91
+ end
92
+
93
+ def load_config
94
+ Config.new(
95
+ profile: parent_options[:profile],
96
+ public_key: parent_options[:public_key],
97
+ secret_key: parent_options[:secret_key],
98
+ host: parent_options[:host],
99
+ format: parent_options[:format],
100
+ limit: parent_options[:limit] || options[:limit]
101
+ )
102
+ end
103
+
104
+ def build_filters(opts)
105
+ filters = {}
106
+ filters[:trace_id] = opts[:trace_id] if opts[:trace_id]
107
+ filters[:name] = opts[:name] if opts[:name]
108
+ filters[:type] = opts[:type] if opts[:type]
109
+ filters[:user_id] = opts[:user_id] if opts[:user_id]
110
+ filters[:from] = opts[:from] if opts[:from]
111
+ filters[:to] = opts[:to] if opts[:to]
112
+ filters[:limit] = opts[:limit] if opts[:limit]
113
+ filters[:page] = opts[:page] if opts[:page]
114
+ filters
115
+ end
116
+
117
+ def output_result(data)
118
+ formatted = format_output(data)
119
+
120
+ if parent_options[:output]
121
+ File.write(parent_options[:output], formatted)
122
+ puts "Output written to #{parent_options[:output]}" if parent_options[:verbose]
123
+ else
124
+ puts formatted
125
+ end
126
+ end
127
+
128
+ def format_output(data)
129
+ format_type = parent_options[:format] || 'table'
130
+
131
+ case format_type
132
+ when 'json'
133
+ require 'json'
134
+ JSON.pretty_generate(data)
135
+ when 'csv'
136
+ require_relative '../formatters/csv_formatter'
137
+ Formatters::CSVFormatter.format(data)
138
+ when 'markdown'
139
+ require_relative '../formatters/markdown_formatter'
140
+ Formatters::MarkdownFormatter.format(data)
141
+ else # table
142
+ require_relative '../formatters/table_formatter'
143
+ Formatters::TableFormatter.format(data)
144
+ end
145
+ end
146
+
147
+ def parent_options
148
+ @parent_options ||= begin
149
+ if parent.respond_to?(:options)
150
+ parent.options
151
+ else
152
+ {}
153
+ end
154
+ rescue
155
+ {}
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,122 @@
1
+ require 'thor'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Commands
6
+ class Scores < Thor
7
+ namespace :scores
8
+
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc 'list', 'List scores'
14
+ option :name, type: :string, desc: 'Filter by score name'
15
+ option :from, type: :string, desc: 'Start timestamp (ISO 8601 or relative)'
16
+ option :to, type: :string, desc: 'End timestamp (ISO 8601 or relative)'
17
+ option :limit, type: :numeric, desc: 'Limit number of results'
18
+ option :page, type: :numeric, desc: 'Page number'
19
+ def list
20
+ filters = build_filters(options)
21
+ scores = client.list_scores(filters)
22
+ output_result(scores)
23
+ rescue Client::AuthenticationError => e
24
+ puts "Authentication Error: #{e.message}"
25
+ exit 1
26
+ rescue Client::APIError => e
27
+ puts "Error: #{e.message}"
28
+ exit 1
29
+ end
30
+
31
+ desc 'get SCORE_ID', 'Get a specific score'
32
+ def get(score_id)
33
+ score = client.get_score(score_id)
34
+ output_result(score)
35
+ rescue Client::NotFoundError => e
36
+ puts "Error: Score not found - #{score_id}"
37
+ exit 1
38
+ rescue Client::APIError => e
39
+ puts "Error: #{e.message}"
40
+ exit 1
41
+ end
42
+
43
+ private
44
+
45
+ def client
46
+ @client ||= begin
47
+ config = load_config
48
+ unless config.valid?
49
+ error_message = "Missing required configuration: #{config.missing_fields.join(', ')}"
50
+ error_message += "\n\nPlease set environment variables or run: langfuse config setup"
51
+ raise Error, error_message
52
+ end
53
+ Client.new(config)
54
+ end
55
+ end
56
+
57
+ def load_config
58
+ Config.new(
59
+ profile: parent_options[:profile],
60
+ public_key: parent_options[:public_key],
61
+ secret_key: parent_options[:secret_key],
62
+ host: parent_options[:host],
63
+ format: parent_options[:format],
64
+ limit: parent_options[:limit] || options[:limit]
65
+ )
66
+ end
67
+
68
+ def build_filters(opts)
69
+ filters = {}
70
+ filters[:name] = opts[:name] if opts[:name]
71
+ filters[:from] = opts[:from] if opts[:from]
72
+ filters[:to] = opts[:to] if opts[:to]
73
+ filters[:limit] = opts[:limit] if opts[:limit]
74
+ filters[:page] = opts[:page] if opts[:page]
75
+ filters
76
+ end
77
+
78
+ def output_result(data)
79
+ formatted = format_output(data)
80
+
81
+ if parent_options[:output]
82
+ File.write(parent_options[:output], formatted)
83
+ puts "Output written to #{parent_options[:output]}" if parent_options[:verbose]
84
+ else
85
+ puts formatted
86
+ end
87
+ end
88
+
89
+ def format_output(data)
90
+ format_type = parent_options[:format] || 'table'
91
+
92
+ case format_type
93
+ when 'json'
94
+ require 'json'
95
+ JSON.pretty_generate(data)
96
+ when 'csv'
97
+ require_relative '../formatters/csv_formatter'
98
+ Formatters::CSVFormatter.format(data)
99
+ when 'markdown'
100
+ require_relative '../formatters/markdown_formatter'
101
+ Formatters::MarkdownFormatter.format(data)
102
+ else # table
103
+ require_relative '../formatters/table_formatter'
104
+ Formatters::TableFormatter.format(data)
105
+ end
106
+ end
107
+
108
+ def parent_options
109
+ @parent_options ||= begin
110
+ if parent.respond_to?(:options)
111
+ parent.options
112
+ else
113
+ {}
114
+ end
115
+ rescue
116
+ {}
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,121 @@
1
+ require 'thor'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Commands
6
+ class Sessions < Thor
7
+ namespace :sessions
8
+
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc 'list', 'List sessions'
14
+ option :from, type: :string, desc: 'Start timestamp (ISO 8601 or relative)'
15
+ option :to, type: :string, desc: 'End timestamp (ISO 8601 or relative)'
16
+ option :limit, type: :numeric, desc: 'Limit number of results'
17
+ option :page, type: :numeric, desc: 'Page number'
18
+ def list
19
+ filters = build_filters(options)
20
+ sessions = client.list_sessions(filters)
21
+ output_result(sessions)
22
+ rescue Client::AuthenticationError => e
23
+ puts "Authentication Error: #{e.message}"
24
+ exit 1
25
+ rescue Client::APIError => e
26
+ puts "Error: #{e.message}"
27
+ exit 1
28
+ end
29
+
30
+ desc 'show SESSION_ID', 'Show a specific session'
31
+ option :with_traces, type: :boolean, default: false, desc: 'Include all traces'
32
+ def show(session_id)
33
+ session = client.get_session(session_id)
34
+ output_result(session)
35
+ rescue Client::NotFoundError => e
36
+ puts "Error: Session not found - #{session_id}"
37
+ exit 1
38
+ rescue Client::APIError => e
39
+ puts "Error: #{e.message}"
40
+ exit 1
41
+ end
42
+
43
+ private
44
+
45
+ def client
46
+ @client ||= begin
47
+ config = load_config
48
+ unless config.valid?
49
+ error_message = "Missing required configuration: #{config.missing_fields.join(', ')}"
50
+ error_message += "\n\nPlease set environment variables or run: langfuse config setup"
51
+ raise Error, error_message
52
+ end
53
+ Client.new(config)
54
+ end
55
+ end
56
+
57
+ def load_config
58
+ Config.new(
59
+ profile: parent_options[:profile],
60
+ public_key: parent_options[:public_key],
61
+ secret_key: parent_options[:secret_key],
62
+ host: parent_options[:host],
63
+ format: parent_options[:format],
64
+ limit: parent_options[:limit] || options[:limit]
65
+ )
66
+ end
67
+
68
+ def build_filters(opts)
69
+ filters = {}
70
+ filters[:from] = opts[:from] if opts[:from]
71
+ filters[:to] = opts[:to] if opts[:to]
72
+ filters[:limit] = opts[:limit] if opts[:limit]
73
+ filters[:page] = opts[:page] if opts[:page]
74
+ filters
75
+ end
76
+
77
+ def output_result(data)
78
+ formatted = format_output(data)
79
+
80
+ if parent_options[:output]
81
+ File.write(parent_options[:output], formatted)
82
+ puts "Output written to #{parent_options[:output]}" if parent_options[:verbose]
83
+ else
84
+ puts formatted
85
+ end
86
+ end
87
+
88
+ def format_output(data)
89
+ format_type = parent_options[:format] || 'table'
90
+
91
+ case format_type
92
+ when 'json'
93
+ require 'json'
94
+ JSON.pretty_generate(data)
95
+ when 'csv'
96
+ require_relative '../formatters/csv_formatter'
97
+ Formatters::CSVFormatter.format(data)
98
+ when 'markdown'
99
+ require_relative '../formatters/markdown_formatter'
100
+ Formatters::MarkdownFormatter.format(data)
101
+ else # table
102
+ require_relative '../formatters/table_formatter'
103
+ Formatters::TableFormatter.format(data)
104
+ end
105
+ end
106
+
107
+ def parent_options
108
+ @parent_options ||= begin
109
+ if parent.respond_to?(:options)
110
+ parent.options
111
+ else
112
+ {}
113
+ end
114
+ rescue
115
+ {}
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,166 @@
1
+ require 'thor'
2
+
3
+ module Langfuse
4
+ module CLI
5
+ module Commands
6
+ class Traces < Thor
7
+ namespace :traces
8
+
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc 'list', 'List traces'
14
+ long_desc <<-LONGDESC
15
+ List traces with optional filtering.
16
+
17
+ Traces represent complete workflows or conversations in Langfuse.
18
+
19
+ FILTERS:
20
+ --name: Filter by trace name
21
+
22
+ --user-id: Filter by user ID
23
+
24
+ --session-id: Filter by session ID
25
+
26
+ --tags: Filter by tags (repeatable)
27
+
28
+ --from, --to: Time range (ISO 8601 or relative like "1 hour ago")
29
+
30
+ OUTPUT OPTIONS:
31
+ Global options: --format [table|json|csv|markdown], --output FILE
32
+
33
+ EXAMPLES:
34
+
35
+ # List recent traces
36
+ langfuse traces list --from "1 hour ago" --limit 20
37
+
38
+ # Find traces by name
39
+ langfuse traces list --name "chat_completion"
40
+
41
+ # Filter by user and session
42
+ langfuse traces list --user-id user_123 --session-id sess_456
43
+
44
+ # Export to CSV
45
+ langfuse traces list --format csv --output traces.csv
46
+
47
+ API REFERENCE:
48
+ Full API documentation: https://api.reference.langfuse.com/
49
+ LONGDESC
50
+ option :from, type: :string, desc: 'Start timestamp (ISO 8601 or relative like "1 hour ago")'
51
+ option :to, type: :string, desc: 'End timestamp (ISO 8601 or relative)'
52
+ option :name, type: :string, desc: 'Filter by trace name'
53
+ option :user_id, type: :string, desc: 'Filter by user ID'
54
+ option :session_id, type: :string, desc: 'Filter by session ID'
55
+ option :tags, type: :array, desc: 'Filter by tags'
56
+ option :limit, type: :numeric, desc: 'Limit number of results'
57
+ option :page, type: :numeric, desc: 'Page number'
58
+ def list
59
+ filters = build_filters(options)
60
+ traces = client.list_traces(filters)
61
+ output_result(traces)
62
+ rescue Client::AuthenticationError => e
63
+ puts "Authentication Error: #{e.message}"
64
+ exit 1
65
+ rescue Client::APIError => e
66
+ puts "Error: #{e.message}"
67
+ exit 1
68
+ end
69
+
70
+ desc 'get TRACE_ID', 'Get a specific trace'
71
+ option :with_observations, type: :boolean, default: false, desc: 'Include all observations'
72
+ def get(trace_id)
73
+ trace = client.get_trace(trace_id)
74
+ output_result(trace)
75
+ rescue Client::NotFoundError => e
76
+ puts "Error: Trace not found - #{trace_id}"
77
+ exit 1
78
+ rescue Client::APIError => e
79
+ puts "Error: #{e.message}"
80
+ exit 1
81
+ end
82
+
83
+ private
84
+
85
+ def client
86
+ @client ||= begin
87
+ config = load_config
88
+ unless config.valid?
89
+ error_message = "Missing required configuration: #{config.missing_fields.join(', ')}"
90
+ error_message += "\n\nPlease set environment variables or run: langfuse config setup"
91
+ raise Error, error_message
92
+ end
93
+ Client.new(config)
94
+ end
95
+ end
96
+
97
+ def load_config
98
+ Config.new(
99
+ profile: parent_options[:profile],
100
+ public_key: parent_options[:public_key],
101
+ secret_key: parent_options[:secret_key],
102
+ host: parent_options[:host],
103
+ format: parent_options[:format],
104
+ limit: parent_options[:limit] || options[:limit]
105
+ )
106
+ end
107
+
108
+ def build_filters(opts)
109
+ filters = {}
110
+ filters[:from] = opts[:from] if opts[:from]
111
+ filters[:to] = opts[:to] if opts[:to]
112
+ filters[:name] = opts[:name] if opts[:name]
113
+ filters[:user_id] = opts[:user_id] if opts[:user_id]
114
+ filters[:session_id] = opts[:session_id] if opts[:session_id]
115
+ filters[:tags] = opts[:tags] if opts[:tags]
116
+ filters[:limit] = opts[:limit] if opts[:limit]
117
+ filters[:page] = opts[:page] if opts[:page]
118
+ filters
119
+ end
120
+
121
+ def output_result(data)
122
+ formatted = format_output(data)
123
+
124
+ if parent_options[:output]
125
+ File.write(parent_options[:output], formatted)
126
+ puts "Output written to #{parent_options[:output]}" if parent_options[:verbose]
127
+ else
128
+ puts formatted
129
+ end
130
+ end
131
+
132
+ def format_output(data)
133
+ format_type = parent_options[:format] || 'table'
134
+
135
+ case format_type
136
+ when 'json'
137
+ require 'json'
138
+ JSON.pretty_generate(data)
139
+ when 'csv'
140
+ require_relative '../formatters/csv_formatter'
141
+ Formatters::CSVFormatter.format(data)
142
+ when 'markdown'
143
+ require_relative '../formatters/markdown_formatter'
144
+ Formatters::MarkdownFormatter.format(data)
145
+ else # table
146
+ require_relative '../formatters/table_formatter'
147
+ Formatters::TableFormatter.format(data)
148
+ end
149
+ end
150
+
151
+ def parent_options
152
+ @parent_options ||= begin
153
+ # Try to get parent options from Thor
154
+ if parent.respond_to?(:options)
155
+ parent.options
156
+ else
157
+ {}
158
+ end
159
+ rescue
160
+ {}
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,136 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ module Langfuse
5
+ module CLI
6
+ class Config
7
+ attr_accessor :public_key, :secret_key, :host, :profile, :output_format, :page_limit
8
+
9
+ DEFAULT_HOST = 'https://cloud.langfuse.com'
10
+ DEFAULT_OUTPUT_FORMAT = 'table'
11
+ DEFAULT_PAGE_LIMIT = 50
12
+ CONFIG_DIR = File.expand_path('~/.langfuse')
13
+ CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
14
+
15
+ def initialize(options = {})
16
+ @profile = options[:profile] || ENV['LANGFUSE_PROFILE'] || 'default'
17
+ load_config
18
+ merge_options(options)
19
+ end
20
+
21
+ # Load configuration from file and environment variables
22
+ # Priority: passed options > ENV vars > config file > defaults
23
+ def load_config
24
+ # Start with defaults
25
+ @host = DEFAULT_HOST
26
+ @output_format = DEFAULT_OUTPUT_FORMAT
27
+ @page_limit = DEFAULT_PAGE_LIMIT
28
+ @public_key = nil
29
+ @secret_key = nil
30
+
31
+ # Load from config file if it exists
32
+ if File.exist?(CONFIG_FILE)
33
+ load_from_file
34
+ end
35
+
36
+ # Override with environment variables
37
+ load_from_env
38
+ end
39
+
40
+ # Load configuration from YAML file
41
+ def load_from_file
42
+ config_data = YAML.load_file(CONFIG_FILE)
43
+
44
+ # Load profile-specific config
45
+ profile_config = config_data.dig('profiles', @profile) || config_data['default'] || {}
46
+
47
+ @public_key = profile_config['public_key'] if profile_config['public_key']
48
+ @secret_key = profile_config['secret_key'] if profile_config['secret_key']
49
+ @host = profile_config['host'] if profile_config['host']
50
+ @output_format = profile_config['output_format'] if profile_config['output_format']
51
+ @page_limit = profile_config['page_limit'] if profile_config['page_limit']
52
+ rescue => e
53
+ warn "Warning: Error loading config file: #{e.message}"
54
+ end
55
+
56
+ # Load configuration from environment variables
57
+ def load_from_env
58
+ @public_key = ENV['LANGFUSE_PUBLIC_KEY'] if ENV['LANGFUSE_PUBLIC_KEY']
59
+ @secret_key = ENV['LANGFUSE_SECRET_KEY'] if ENV['LANGFUSE_SECRET_KEY']
60
+ @host = ENV['LANGFUSE_HOST'] if ENV['LANGFUSE_HOST']
61
+ end
62
+
63
+ # Merge passed options (highest priority)
64
+ def merge_options(options)
65
+ @public_key = options[:public_key] if options[:public_key]
66
+ @secret_key = options[:secret_key] if options[:secret_key]
67
+ @host = options[:host] if options[:host]
68
+ @output_format = options[:format] if options[:format]
69
+ @page_limit = options[:limit] if options[:limit]
70
+ end
71
+
72
+ # Validate that required configuration is present
73
+ def valid?
74
+ !@public_key.nil? && !@secret_key.nil? && !@host.nil?
75
+ end
76
+
77
+ # Get list of missing required fields
78
+ def missing_fields
79
+ fields = []
80
+ fields << 'public_key' if @public_key.nil?
81
+ fields << 'secret_key' if @secret_key.nil?
82
+ fields << 'host' if @host.nil?
83
+ fields
84
+ end
85
+
86
+ # Save current configuration to file
87
+ def save(profile_name = nil)
88
+ profile_name ||= @profile
89
+
90
+ # Ensure config directory exists
91
+ FileUtils.mkdir_p(CONFIG_DIR)
92
+
93
+ # Load existing config or create new
94
+ config_data = File.exist?(CONFIG_FILE) ? YAML.load_file(CONFIG_FILE) : {}
95
+ config_data['profiles'] ||= {}
96
+
97
+ # Update profile
98
+ config_data['profiles'][profile_name] = {
99
+ 'public_key' => @public_key,
100
+ 'secret_key' => @secret_key,
101
+ 'host' => @host,
102
+ 'output_format' => @output_format,
103
+ 'page_limit' => @page_limit
104
+ }
105
+
106
+ # Write to file
107
+ File.write(CONFIG_FILE, config_data.to_yaml)
108
+
109
+ # Set restrictive permissions
110
+ File.chmod(0600, CONFIG_FILE)
111
+
112
+ true
113
+ rescue => e
114
+ warn "Error saving config: #{e.message}"
115
+ false
116
+ end
117
+
118
+ # Load a specific profile
119
+ def self.load(profile = nil)
120
+ new(profile: profile)
121
+ end
122
+
123
+ # Get configuration as a hash
124
+ def to_h
125
+ {
126
+ public_key: @public_key,
127
+ secret_key: @secret_key,
128
+ host: @host,
129
+ profile: @profile,
130
+ output_format: @output_format,
131
+ page_limit: @page_limit
132
+ }
133
+ end
134
+ end
135
+ end
136
+ end