langfuse-ruby 0.1.2 → 0.1.3

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.
@@ -24,23 +24,6 @@ module Langfuse
24
24
  create_trace
25
25
  end
26
26
 
27
- def update(name: nil, user_id: nil, session_id: nil, version: nil, release: nil,
28
- input: nil, output: nil, metadata: nil, tags: nil, **kwargs)
29
- @name = name if name
30
- @user_id = user_id if user_id
31
- @session_id = session_id if session_id
32
- @version = version if version
33
- @release = release if release
34
- @input = input if input
35
- @output = output if output
36
- @metadata.merge!(metadata) if metadata
37
- @tags.concat(tags) if tags
38
- @kwargs.merge!(kwargs)
39
-
40
- update_trace
41
- self
42
- end
43
-
44
27
  def span(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
45
28
  metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
46
29
  version: nil, **kwargs)
@@ -84,6 +67,23 @@ module Langfuse
84
67
  )
85
68
  end
86
69
 
70
+ def event(name:, start_time: nil, input: nil, output: nil, metadata: nil,
71
+ level: nil, status_message: nil, parent_observation_id: nil, version: nil, **kwargs)
72
+ @client.event(
73
+ trace_id: @id,
74
+ name: name,
75
+ start_time: start_time,
76
+ input: input,
77
+ output: output,
78
+ metadata: metadata,
79
+ level: level,
80
+ status_message: status_message,
81
+ parent_observation_id: parent_observation_id,
82
+ version: version,
83
+ **kwargs
84
+ )
85
+ end
86
+
87
87
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
88
88
  @client.score(
89
89
  trace_id: @id,
@@ -134,22 +134,5 @@ module Langfuse
134
134
 
135
135
  @client.enqueue_event('trace-create', data)
136
136
  end
137
-
138
- def update_trace
139
- data = {
140
- id: @id,
141
- name: @name,
142
- user_id: @user_id,
143
- session_id: @session_id,
144
- version: @version,
145
- release: @release,
146
- input: @input,
147
- output: @output,
148
- metadata: @metadata,
149
- tags: @tags
150
- }.merge(@kwargs).compact
151
-
152
- @client.enqueue_event('trace-update', data)
153
- end
154
137
  end
155
138
  end
@@ -26,12 +26,34 @@ module Langfuse
26
26
  return hash unless hash.is_a?(Hash)
27
27
 
28
28
  hash.each_with_object({}) do |(key, value), result|
29
- new_key = key.to_s
29
+ new_key = camelize_key(key.to_s)
30
30
  new_value = value.is_a?(Hash) ? deep_stringify_keys(value) : value
31
31
  result[new_key] = new_value
32
32
  end
33
33
  end
34
34
 
35
+ # 将哈希的键名转换为小驼峰格式
36
+ def deep_camelize_keys(hash)
37
+ return hash unless hash.is_a?(Hash)
38
+
39
+ hash.each_with_object({}) do |(key, value), result|
40
+ new_key = camelize_key(key.to_s)
41
+ new_value = value.is_a?(Hash) ? deep_camelize_keys(value) : value
42
+ result[new_key] = new_value
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # 将蛇形命名转换为小驼峰命名
49
+ def camelize_key(key)
50
+ return key if key.empty? || !key.include?('_')
51
+
52
+ key.split('_').map.with_index do |part, index|
53
+ index.zero? ? part.downcase : part.capitalize
54
+ end.join
55
+ end
56
+
35
57
  def validate_required_fields(data, required_fields)
36
58
  missing_fields = required_fields.select { |field| data[field].nil? || data[field].to_s.empty? }
37
59
  raise ValidationError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
@@ -1,3 +1,3 @@
1
1
  module Langfuse
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
data/lib/langfuse.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'langfuse/client'
5
5
  require_relative 'langfuse/trace'
6
6
  require_relative 'langfuse/span'
7
7
  require_relative 'langfuse/generation'
8
+ require_relative 'langfuse/event'
8
9
  require_relative 'langfuse/prompt'
9
10
  require_relative 'langfuse/evaluation'
10
11
  require_relative 'langfuse/errors'
@@ -30,7 +31,7 @@ module Langfuse
30
31
 
31
32
  # Configuration class for Langfuse client settings
32
33
  class Configuration
33
- attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries
34
+ attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush
34
35
 
35
36
  def initialize
36
37
  @public_key = nil
@@ -39,6 +40,8 @@ module Langfuse
39
40
  @debug = false
40
41
  @timeout = 30
41
42
  @retries = 3
43
+ @flush_interval = 5
44
+ @auto_flush = true
42
45
  end
43
46
  end
44
47
  end
data/test_offline.rb CHANGED
@@ -2,30 +2,30 @@
2
2
 
3
3
  require_relative 'lib/langfuse'
4
4
 
5
- puts "🚀 Testing Langfuse Ruby SDK (Offline Mode)..."
5
+ puts '🚀 Testing Langfuse Ruby SDK (Offline Mode)...'
6
6
 
7
7
  # Test 1: Basic configuration
8
8
  puts "\n1. Testing configuration..."
9
9
  Langfuse.configure do |config|
10
- config.public_key = "test_key"
11
- config.secret_key = "test_secret"
12
- config.host = "https://test.langfuse.com"
13
- config.debug = false # Turn off debug to avoid network calls
10
+ config.public_key = 'test_key'
11
+ config.secret_key = 'test_secret'
12
+ config.host = 'https://test.langfuse.com'
13
+ config.debug = false # Turn off debug to avoid network calls
14
14
  end
15
15
 
16
- puts "✅ Configuration successful"
16
+ puts '✅ Configuration successful'
17
17
 
18
18
  # Test 2: Client initialization
19
19
  puts "\n2. Testing client initialization..."
20
20
  begin
21
21
  client = Langfuse.new(
22
- public_key: "test_key",
23
- secret_key: "test_secret",
24
- host: "https://test.langfuse.com",
22
+ public_key: 'test_key',
23
+ secret_key: 'test_secret',
24
+ host: 'https://test.langfuse.com',
25
25
  debug: false
26
26
  )
27
- puts "✅ Client initialization successful"
28
- rescue => e
27
+ puts '✅ Client initialization successful'
28
+ rescue StandardError => e
29
29
  puts "❌ Client initialization failed: #{e.message}"
30
30
  end
31
31
 
@@ -33,13 +33,13 @@ end
33
33
  puts "\n3. Testing trace creation..."
34
34
  begin
35
35
  trace = client.trace(
36
- name: "test-trace",
37
- user_id: "test-user",
38
- input: { message: "Hello, world!" }
36
+ name: 'test-trace',
37
+ user_id: 'test-user',
38
+ input: { message: 'Hello, world!' }
39
39
  )
40
- puts "✅ Trace creation successful"
40
+ puts '✅ Trace creation successful'
41
41
  puts " Trace ID: #{trace.id}"
42
- rescue => e
42
+ rescue StandardError => e
43
43
  puts "❌ Trace creation failed: #{e.message}"
44
44
  end
45
45
 
@@ -47,14 +47,14 @@ end
47
47
  puts "\n4. Testing generation creation..."
48
48
  begin
49
49
  generation = trace.generation(
50
- name: "test-generation",
51
- model: "gpt-3.5-turbo",
52
- input: [{ role: "user", content: "Hello!" }],
53
- output: { content: "Hi there!" }
50
+ name: 'test-generation',
51
+ model: 'gpt-3.5-turbo',
52
+ input: [{ role: 'user', content: 'Hello!' }],
53
+ output: { content: 'Hi there!' }
54
54
  )
55
- puts "✅ Generation creation successful"
55
+ puts '✅ Generation creation successful'
56
56
  puts " Generation ID: #{generation.id}"
57
- rescue => e
57
+ rescue StandardError => e
58
58
  puts "❌ Generation creation failed: #{e.message}"
59
59
  end
60
60
 
@@ -62,18 +62,18 @@ end
62
62
  puts "\n5. Testing prompt template..."
63
63
  begin
64
64
  template = Langfuse::PromptTemplate.from_template(
65
- "Hello {{name}}! How are you feeling {{mood}} today?"
65
+ 'Hello {{name}}! How are you feeling {{mood}} today?'
66
66
  )
67
67
 
68
68
  formatted = template.format(
69
- name: "Alice",
70
- mood: "happy"
69
+ name: 'Alice',
70
+ mood: 'happy'
71
71
  )
72
72
 
73
- puts "✅ Prompt template successful"
73
+ puts '✅ Prompt template successful'
74
74
  puts " Variables: #{template.input_variables}"
75
75
  puts " Formatted: #{formatted}"
76
- rescue => e
76
+ rescue StandardError => e
77
77
  puts "❌ Prompt template failed: #{e.message}"
78
78
  end
79
79
 
@@ -81,19 +81,20 @@ end
81
81
  puts "\n6. Testing chat prompt template..."
82
82
  begin
83
83
  chat_template = Langfuse::ChatPromptTemplate.from_messages([
84
- { role: "system", content: "You are a helpful {{role}} assistant." },
85
- { role: "user", content: "{{user_input}}" }
86
- ])
84
+ { role: 'system',
85
+ content: 'You are a helpful {{role}} assistant.' },
86
+ { role: 'user', content: '{{user_input}}' }
87
+ ])
87
88
 
88
89
  messages = chat_template.format(
89
- role: "coding",
90
- user_input: "Help me with Ruby"
90
+ role: 'coding',
91
+ user_input: 'Help me with Ruby'
91
92
  )
92
93
 
93
- puts "✅ Chat prompt template successful"
94
+ puts '✅ Chat prompt template successful'
94
95
  puts " Variables: #{chat_template.input_variables}"
95
96
  puts " Messages: #{messages.length}"
96
- rescue => e
97
+ rescue StandardError => e
97
98
  puts "❌ Chat prompt template failed: #{e.message}"
98
99
  end
99
100
 
@@ -103,26 +104,26 @@ begin
103
104
  # Exact match evaluator
104
105
  exact_match = Langfuse::Evaluators::ExactMatchEvaluator.new
105
106
  result = exact_match.evaluate(
106
- "What is 2+2?",
107
- "4",
108
- expected: "4"
107
+ 'What is 2+2?',
108
+ '4',
109
+ expected: '4'
109
110
  )
110
111
  puts "✅ Exact match evaluator: #{result}"
111
112
 
112
113
  # Similarity evaluator
113
114
  similarity = Langfuse::Evaluators::SimilarityEvaluator.new
114
115
  result = similarity.evaluate(
115
- "What is AI?",
116
- "Artificial Intelligence",
117
- expected: "AI is artificial intelligence"
116
+ 'What is AI?',
117
+ 'Artificial Intelligence',
118
+ expected: 'AI is artificial intelligence'
118
119
  )
119
120
  puts "✅ Similarity evaluator: #{result}"
120
121
 
121
122
  # Length evaluator
122
123
  length = Langfuse::Evaluators::LengthEvaluator.new(min_length: 5, max_length: 20)
123
124
  result = length.evaluate(
124
- "Test input",
125
- "This is a test",
125
+ 'Test input',
126
+ 'This is a test',
126
127
  expected: nil
127
128
  )
128
129
  puts "✅ Length evaluator: #{result}"
@@ -130,22 +131,21 @@ begin
130
131
  # Contains evaluator
131
132
  contains = Langfuse::Evaluators::ContainsEvaluator.new
132
133
  result = contains.evaluate(
133
- "Find Ruby",
134
- "Ruby programming language",
135
- expected: "Ruby"
134
+ 'Find Ruby',
135
+ 'Ruby programming language',
136
+ expected: 'Ruby'
136
137
  )
137
138
  puts "✅ Contains evaluator: #{result}"
138
139
 
139
140
  # Regex evaluator
140
141
  regex = Langfuse::Evaluators::RegexEvaluator.new(pattern: /\d+/)
141
142
  result = regex.evaluate(
142
- "Find numbers",
143
- "There are 42 apples",
143
+ 'Find numbers',
144
+ 'There are 42 apples',
144
145
  expected: nil
145
146
  )
146
147
  puts "✅ Regex evaluator: #{result}"
147
-
148
- rescue => e
148
+ rescue StandardError => e
149
149
  puts "❌ Evaluator failed: #{e.message}"
150
150
  end
151
151
 
@@ -156,16 +156,16 @@ begin
156
156
  timestamp = Langfuse::Utils.current_timestamp
157
157
 
158
158
  # Test data transformation
159
- hash_data = { "key1" => "value1", "nested" => { "key2" => "value2" } }
159
+ hash_data = { 'key1' => 'value1', 'nested' => { 'key2' => 'value2' } }
160
160
  symbolized = Langfuse::Utils.deep_symbolize_keys(hash_data)
161
161
  stringified = Langfuse::Utils.deep_stringify_keys(symbolized)
162
162
 
163
- puts "✅ Utils successful"
163
+ puts '✅ Utils successful'
164
164
  puts " ID: #{id[0..8]}..."
165
165
  puts " Timestamp: #{timestamp}"
166
166
  puts " Symbolized keys: #{symbolized.keys}"
167
167
  puts " Stringified keys: #{stringified.keys}"
168
- rescue => e
168
+ rescue StandardError => e
169
169
  puts "❌ Utils failed: #{e.message}"
170
170
  end
171
171
 
@@ -177,26 +177,26 @@ begin
177
177
  # Add some events
178
178
  client.score(
179
179
  trace_id: trace.id,
180
- name: "test-score",
180
+ name: 'test-score',
181
181
  value: 0.9
182
182
  )
183
183
 
184
184
  trace.score(
185
- name: "trace-score",
185
+ name: 'trace-score',
186
186
  value: 0.8
187
187
  )
188
188
 
189
189
  generation.score(
190
- name: "generation-score",
190
+ name: 'generation-score',
191
191
  value: 0.95
192
192
  )
193
193
 
194
194
  queue_size_after = client.instance_variable_get(:@event_queue).length
195
195
 
196
- puts "✅ Event queue successful"
196
+ puts '✅ Event queue successful'
197
197
  puts " Events added: #{queue_size_after - queue_size_before}"
198
198
  puts " Total events: #{queue_size_after}"
199
- rescue => e
199
+ rescue StandardError => e
200
200
  puts "❌ Event queue failed: #{e.message}"
201
201
  end
202
202
 
@@ -205,28 +205,28 @@ puts "\n10. Testing complex workflow..."
205
205
  begin
206
206
  # Create a complex trace with nested spans
207
207
  complex_trace = client.trace(
208
- name: "complex-workflow",
209
- user_id: "user-456",
210
- session_id: "session-789",
211
- input: { query: "Explain quantum computing" },
208
+ name: 'complex-workflow',
209
+ user_id: 'user-456',
210
+ session_id: 'session-789',
211
+ input: { query: 'Explain quantum computing' },
212
212
  metadata: {
213
- environment: "test",
214
- version: "1.0.0",
215
- tags: ["physics", "computing"]
213
+ environment: 'test',
214
+ version: '1.0.0',
215
+ tags: %w[physics computing]
216
216
  }
217
217
  )
218
218
 
219
219
  # Document retrieval span
220
220
  retrieval_span = complex_trace.span(
221
- name: "document-retrieval",
222
- input: { query: "quantum computing basics" }
221
+ name: 'document-retrieval',
222
+ input: { query: 'quantum computing basics' }
223
223
  )
224
224
 
225
225
  # Embedding generation
226
226
  embedding_gen = retrieval_span.generation(
227
- name: "embedding-generation",
228
- model: "text-embedding-ada-002",
229
- input: "quantum computing basics",
227
+ name: 'embedding-generation',
228
+ model: 'text-embedding-ada-002',
229
+ input: 'quantum computing basics',
230
230
  output: Array.new(1536) { rand(-1.0..1.0) }, # Mock embedding
231
231
  usage: { prompt_tokens: 4, total_tokens: 4 }
232
232
  )
@@ -234,31 +234,31 @@ begin
234
234
  retrieval_span.end(
235
235
  output: {
236
236
  documents: [
237
- "Quantum computing uses quantum bits...",
238
- "Quantum algorithms can solve certain problems..."
237
+ 'Quantum computing uses quantum bits...',
238
+ 'Quantum algorithms can solve certain problems...'
239
239
  ]
240
240
  }
241
241
  )
242
242
 
243
243
  # Answer generation span
244
244
  answer_span = complex_trace.span(
245
- name: "answer-generation",
245
+ name: 'answer-generation',
246
246
  input: {
247
- query: "Explain quantum computing",
248
- context: ["Quantum computing uses quantum bits...", "Quantum algorithms can solve certain problems..."]
247
+ query: 'Explain quantum computing',
248
+ context: ['Quantum computing uses quantum bits...', 'Quantum algorithms can solve certain problems...']
249
249
  }
250
250
  )
251
251
 
252
252
  # LLM generation
253
253
  llm_gen = answer_span.generation(
254
- name: "openai-completion",
255
- model: "gpt-4",
254
+ name: 'openai-completion',
255
+ model: 'gpt-4',
256
256
  input: [
257
- { role: "system", content: "You are a physics expert." },
258
- { role: "user", content: "Explain quantum computing based on the context." }
257
+ { role: 'system', content: 'You are a physics expert.' },
258
+ { role: 'user', content: 'Explain quantum computing based on the context.' }
259
259
  ],
260
260
  output: {
261
- content: "Quantum computing is a revolutionary approach to computation that leverages quantum mechanical phenomena like superposition and entanglement to process information in fundamentally different ways than classical computers."
261
+ content: 'Quantum computing is a revolutionary approach to computation that leverages quantum mechanical phenomena like superposition and entanglement to process information in fundamentally different ways than classical computers.'
262
262
  },
263
263
  usage: {
264
264
  prompt_tokens: 120,
@@ -273,22 +273,15 @@ begin
273
273
 
274
274
  answer_span.end(
275
275
  output: {
276
- answer: "Quantum computing is a revolutionary approach to computation that leverages quantum mechanical phenomena like superposition and entanglement to process information in fundamentally different ways than classical computers."
276
+ answer: 'Quantum computing is a revolutionary approach to computation that leverages quantum mechanical phenomena like superposition and entanglement to process information in fundamentally different ways than classical computers.'
277
277
  }
278
278
  )
279
279
 
280
- complex_trace.update(
281
- output: {
282
- answer: "Quantum computing is a revolutionary approach to computation that leverages quantum mechanical phenomena like superposition and entanglement to process information in fundamentally different ways than classical computers."
283
- }
284
- )
285
-
286
- puts "✅ Complex workflow successful"
280
+ puts '✅ Complex workflow successful'
287
281
  puts " Trace ID: #{complex_trace.id}"
288
- puts " Spans created: 2"
289
- puts " Generations created: 2"
290
-
291
- rescue => e
282
+ puts ' Spans created: 2'
283
+ puts ' Generations created: 2'
284
+ rescue StandardError => e
292
285
  puts "❌ Complex workflow failed: #{e.message}"
293
286
  end
294
287
 
@@ -297,33 +290,32 @@ puts "\n11. Testing error handling..."
297
290
  begin
298
291
  # Test validation error
299
292
  begin
300
- Langfuse::Utils.validate_required_fields({ name: "test" }, [:name, :required_field])
293
+ Langfuse::Utils.validate_required_fields({ name: 'test' }, %i[name required_field])
301
294
  rescue Langfuse::ValidationError => e
302
295
  puts "✅ Validation error caught: #{e.message}"
303
296
  end
304
297
 
305
298
  # Test authentication error
306
299
  begin
307
- Langfuse.new(public_key: nil, secret_key: "secret")
300
+ Langfuse.new(public_key: nil, secret_key: 'secret')
308
301
  rescue Langfuse::AuthenticationError => e
309
302
  puts "✅ Authentication error caught: #{e.message}"
310
303
  end
311
-
312
- rescue => e
304
+ rescue StandardError => e
313
305
  puts "❌ Error handling failed: #{e.message}"
314
306
  end
315
307
 
316
308
  # Final summary
317
309
  puts "\n🎉 All offline tests completed successfully!"
318
- puts " The Langfuse Ruby SDK is ready for use!"
310
+ puts ' The Langfuse Ruby SDK is ready for use!'
319
311
  puts " Total events in queue: #{client.instance_variable_get(:@event_queue).length}"
320
312
 
321
313
  # Clean shutdown (kill background thread)
322
314
  client.instance_variable_get(:@flush_thread)&.kill
323
- puts " Background thread terminated"
315
+ puts ' Background thread terminated'
324
316
 
325
317
  puts "\n📚 Next steps:"
326
- puts " 1. Set your real Langfuse API keys"
327
- puts " 2. Configure the correct host URL"
328
- puts " 3. Start using the SDK in your application"
329
- puts " 4. Check the examples/ directory for more usage patterns"
318
+ puts ' 1. Set your real Langfuse API keys'
319
+ puts ' 2. Configure the correct host URL'
320
+ puts ' 3. Start using the SDK in your application'
321
+ puts ' 4. Check the examples/ directory for more usage patterns'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langfuse-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Sun
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-12 00:00:00.000000000 Z
11
+ date: 2025-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -172,7 +172,6 @@ executables: []
172
172
  extensions: []
173
173
  extra_rdoc_files: []
174
174
  files:
175
- - ".github/workflows/ci.yml"
176
175
  - ".github/workflows/release.yml"
177
176
  - ".gitignore"
178
177
  - CHANGELOG.md
@@ -185,14 +184,18 @@ files:
185
184
  - docs/PUBLISH_GUIDE.md
186
185
  - docs/README.md
187
186
  - docs/RELEASE_CHECKLIST.md
187
+ - docs/TYPE_VALIDATION_TROUBLESHOOTING.md
188
+ - examples/auto_flush_control.rb
188
189
  - examples/basic_tracing.rb
189
190
  - examples/connection_config_demo.rb
191
+ - examples/event_usage.rb
190
192
  - examples/prompt_management.rb
191
193
  - langfuse-ruby.gemspec
192
194
  - lib/langfuse.rb
193
195
  - lib/langfuse/client.rb
194
196
  - lib/langfuse/errors.rb
195
197
  - lib/langfuse/evaluation.rb
198
+ - lib/langfuse/event.rb
196
199
  - lib/langfuse/generation.rb
197
200
  - lib/langfuse/prompt.rb
198
201
  - lib/langfuse/span.rb
@@ -1,47 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- ruby-version: ['2.7', '3.0', '3.1', '3.2']
15
-
16
- steps:
17
- - uses: actions/checkout@v4
18
-
19
- - name: Set up Ruby ${{ matrix.ruby-version }}
20
- uses: ruby/setup-ruby@v1
21
- with:
22
- ruby-version: ${{ matrix.ruby-version }}
23
- bundler-cache: true
24
-
25
- - name: Run tests
26
- run: bundle exec rspec
27
-
28
- - name: Run offline tests
29
- run: ruby test_offline.rb
30
-
31
- - name: Check gem build
32
- run: gem build langfuse.gemspec
33
-
34
- lint:
35
- runs-on: ubuntu-latest
36
- steps:
37
- - uses: actions/checkout@v4
38
-
39
- - name: Set up Ruby
40
- uses: ruby/setup-ruby@v1
41
- with:
42
- ruby-version: '3.2'
43
- bundler-cache: true
44
-
45
- - name: Run RuboCop
46
- run: bundle exec rubocop
47
- continue-on-error: true