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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +399 -0
- data/bin/lf +15 -0
- data/lib/langfuse/cli/client.rb +233 -0
- data/lib/langfuse/cli/commands/config.rb +176 -0
- data/lib/langfuse/cli/commands/metrics.rb +193 -0
- data/lib/langfuse/cli/commands/observations.rb +161 -0
- data/lib/langfuse/cli/commands/scores.rb +122 -0
- data/lib/langfuse/cli/commands/sessions.rb +121 -0
- data/lib/langfuse/cli/commands/traces.rb +166 -0
- data/lib/langfuse/cli/config.rb +136 -0
- data/lib/langfuse/cli/formatters/csv_formatter.rb +46 -0
- data/lib/langfuse/cli/formatters/markdown_formatter.rb +56 -0
- data/lib/langfuse/cli/formatters/table_formatter.rb +43 -0
- data/lib/langfuse/cli/types.rb +98 -0
- data/lib/langfuse/cli/version.rb +5 -0
- data/lib/langfuse/cli.rb +142 -0
- data/lib/langfuse_cli.rb +10 -0
- metadata +303 -0
|
@@ -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
|