langfuse-rb 0.10.0 → 0.11.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/lib/langfuse/api_client.rb +175 -509
  4. data/lib/langfuse/app_root_tracking.rb +164 -0
  5. data/lib/langfuse/cache_warmer.rb +21 -13
  6. data/lib/langfuse/chat_prompt_client.rb +21 -4
  7. data/lib/langfuse/client.rb +174 -229
  8. data/lib/langfuse/config.rb +308 -65
  9. data/lib/langfuse/evaluation.rb +8 -4
  10. data/lib/langfuse/exit_hook.rb +77 -0
  11. data/lib/langfuse/fork_safety.rb +71 -0
  12. data/lib/langfuse/masking_exporter.rb +98 -0
  13. data/lib/langfuse/observations.rb +2 -1
  14. data/lib/langfuse/otel_attributes.rb +1 -0
  15. data/lib/langfuse/otel_setup.rb +32 -44
  16. data/lib/langfuse/otel_span_batch.rb +113 -0
  17. data/lib/langfuse/otel_span_masking.rb +89 -0
  18. data/lib/langfuse/otel_span_patch_applier.rb +97 -0
  19. data/lib/langfuse/pending_score_queue.rb +62 -0
  20. data/lib/langfuse/prompt_cache.rb +11 -0
  21. data/lib/langfuse/prompt_cache_coordinator.rb +288 -0
  22. data/lib/langfuse/prompt_cache_events.rb +31 -10
  23. data/lib/langfuse/prompt_variables.rb +54 -0
  24. data/lib/langfuse/propagation.rb +101 -34
  25. data/lib/langfuse/rails_cache_adapter.rb +27 -2
  26. data/lib/langfuse/read_api.rb +242 -0
  27. data/lib/langfuse/resilient_metrics_reporter.rb +60 -0
  28. data/lib/langfuse/score_client.rb +209 -89
  29. data/lib/langfuse/score_value.rb +58 -0
  30. data/lib/langfuse/span_processor.rb +53 -4
  31. data/lib/langfuse/stale_while_revalidate.rb +3 -4
  32. data/lib/langfuse/text_prompt_client.rb +15 -6
  33. data/lib/langfuse/trace_export_guard.rb +47 -0
  34. data/lib/langfuse/traced_execution.rb +18 -12
  35. data/lib/langfuse/types.rb +15 -1
  36. data/lib/langfuse/version.rb +1 -1
  37. data/lib/langfuse.rb +157 -44
  38. metadata +16 -2
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Langfuse
4
6
  # Main client for Langfuse SDK
5
7
  #
@@ -19,14 +21,56 @@ module Langfuse
19
21
  #
20
22
  # rubocop:disable Metrics/ClassLength
21
23
  class Client
24
+ extend Forwardable
25
+
22
26
  # @return [Integer] Default page size when fetching all dataset items
23
27
  DATASET_ITEMS_PAGE_SIZE = 50
24
28
 
25
29
  # @return [Config] The client configuration
26
30
  attr_reader :config
27
31
 
32
+ # Return the underlying API client, building it on first non-telemetry use.
33
+ #
28
34
  # @return [ApiClient] The underlying API client
29
- attr_reader :api_client
35
+ # @raise [ConfigurationError] if the full client configuration is invalid
36
+ def api_client
37
+ @api_client || @api_client_mutex.synchronize { @api_client ||= build_validated_api_client }
38
+ end
39
+
40
+ # Pure pass-throughs to {ApiClient}. See {ApiClient} for parameter and
41
+ # return-value documentation; the public surface here is identical.
42
+ #
43
+ # @!method list_prompts(page: nil, limit: nil)
44
+ # @!method invalidate_prompt_cache(name, version: nil, label: nil)
45
+ # @!method invalidate_prompt_cache_by_name(name)
46
+ # @!method clear_prompt_cache
47
+ # @!method prompt_cache_stats
48
+ # @!method prompt_cache_key(name, version: nil, label: nil)
49
+ # @!method validate_prompt_cache_backend!
50
+ # @!method list_traces(**options)
51
+ # @!method get_trace(id)
52
+ # @!method list_observations(**options)
53
+ # @!method query_metrics(query:)
54
+ # @!method list_scores(**options)
55
+ # @!method list_datasets(page: nil, limit: nil)
56
+ # @!method get_dataset_run(dataset_name:, run_name:)
57
+ # @!method create_dataset_run_item(**)
58
+ def_delegators :api_client,
59
+ :list_prompts,
60
+ :invalidate_prompt_cache,
61
+ :invalidate_prompt_cache_by_name,
62
+ :clear_prompt_cache,
63
+ :prompt_cache_stats,
64
+ :prompt_cache_key,
65
+ :validate_prompt_cache_backend!,
66
+ :list_traces,
67
+ :get_trace,
68
+ :list_observations,
69
+ :query_metrics,
70
+ :list_scores,
71
+ :list_datasets,
72
+ :get_dataset_run,
73
+ :create_dataset_run_item
30
74
 
31
75
  # Initialize a new Langfuse client
32
76
  #
@@ -34,29 +78,19 @@ module Langfuse
34
78
  # @return [Client]
35
79
  def initialize(config)
36
80
  @config = config
37
- @config.validate!
38
-
39
- # Create cache if enabled
40
- cache = create_cache if cache_enabled?
41
-
42
- # Create API client with cache
43
- @api_client = ApiClient.new(
44
- public_key: config.public_key,
45
- secret_key: config.secret_key,
46
- base_url: config.base_url,
47
- timeout: config.timeout,
48
- logger: config.logger,
49
- cache: cache,
50
- cache_observer: config.prompt_cache_observer
51
- )
52
-
81
+ @api_client_mutex = Mutex.new
82
+ @score_client_mutex = Mutex.new
53
83
  @project_id = nil
54
84
  # One-shot lookup: avoids repeated blocking API calls in URL helpers
55
85
  # (trace_url, dataset_url, dataset_run_url) when the project endpoint is down.
56
86
  @project_id_fetched = false
57
-
58
- # Initialize score client for batching score events
59
- @score_client = ScoreClient.new(api_client: @api_client, config: config)
87
+ if telemetry_enabled?
88
+ config.validate!
89
+ @api_client = build_api_client
90
+ @score_client = build_score_client
91
+ else
92
+ config.validate_telemetry_disabled!
93
+ end
60
94
  end
61
95
 
62
96
  # Fetch a prompt and return the appropriate client
@@ -134,79 +168,6 @@ module Langfuse
134
168
  build_client_fetch_result(api_result, build_prompt_client(api_result.prompt))
135
169
  end
136
170
 
137
- # Invalidate one exact logical prompt cache key.
138
- #
139
- # @param name [String] The prompt name
140
- # @param version [Integer, nil] Optional specific version number
141
- # @param label [String, nil] Optional label
142
- # @return [PromptCacheKey] The invalidated key
143
- def invalidate_prompt_cache(name, version: nil, label: nil)
144
- api_client.invalidate_prompt_cache(name, version: version, label: label)
145
- end
146
-
147
- # Invalidate all cached variants for one prompt name.
148
- #
149
- # @param name [String] The prompt name
150
- # @return [Integer, nil] New generation, or nil when cache is disabled
151
- def invalidate_prompt_cache_by_name(name)
152
- api_client.invalidate_prompt_cache_by_name(name)
153
- end
154
-
155
- # Logically clear the whole Langfuse prompt cache namespace.
156
- #
157
- # @return [Integer, nil] New global generation, or nil when cache is disabled
158
- def clear_prompt_cache
159
- api_client.clear_prompt_cache
160
- end
161
-
162
- # Return prompt cache statistics.
163
- #
164
- # @return [Hash] Cache statistics
165
- def prompt_cache_stats
166
- api_client.prompt_cache_stats
167
- end
168
-
169
- # Inspect the logical and generated cache keys for a prompt.
170
- #
171
- # @param name [String] The prompt name
172
- # @param version [Integer, nil] Optional specific version number
173
- # @param label [String, nil] Optional label
174
- # @return [PromptCacheKey] Logical and generated cache keys
175
- def prompt_cache_key(name, version: nil, label: nil)
176
- api_client.prompt_cache_key(name, version: version, label: label)
177
- end
178
-
179
- # Validate the configured prompt cache backend before first prompt fetch.
180
- #
181
- # @return [Boolean] true when the configured backend is usable
182
- # @raise [ConfigurationError] if the backend is invalid
183
- # rubocop:disable Naming/PredicateMethod
184
- def validate_prompt_cache_backend!
185
- api_client.cache&.validate! if api_client.cache.respond_to?(:validate!)
186
- true
187
- end
188
- # rubocop:enable Naming/PredicateMethod
189
-
190
- # List all prompts in the Langfuse project
191
- #
192
- # Fetches a list of all prompt names available in your project.
193
- # Returns metadata only, not full prompt content.
194
- #
195
- # @param page [Integer, nil] Optional page number for pagination
196
- # @param limit [Integer, nil] Optional limit per page
197
- # @return [Array<Hash>] Array of prompt metadata hashes
198
- # @raise [UnauthorizedError] if authentication fails
199
- # @raise [ApiError] for other API errors
200
- #
201
- # @example
202
- # prompts = client.list_prompts
203
- # prompts.each do |prompt|
204
- # puts "#{prompt['name']} (v#{prompt['version']})"
205
- # end
206
- def list_prompts(page: nil, limit: nil)
207
- api_client.list_prompts(page: page, limit: limit)
208
- end
209
-
210
171
  # Convenience method: fetch and compile a prompt in one call
211
172
  #
212
173
  # This is a shorthand for calling get_prompt followed by compile.
@@ -387,14 +348,14 @@ module Langfuse
387
348
  #
388
349
  # @param name [String] Score name (required)
389
350
  # @param value [Numeric, Integer, String] Score value (type depends on data_type)
390
- # @param id [String, nil] Score ID
351
+ # @param id [String, nil] Score ID; use a stable value as an idempotency key
391
352
  # @param trace_id [String, nil] Trace ID to associate with the score
392
353
  # @param session_id [String, nil] Session ID to associate with the score
393
354
  # @param observation_id [String, nil] Observation ID to associate with the score
394
355
  # @param comment [String, nil] Optional comment
395
356
  # @param metadata [Hash, nil] Optional metadata hash
396
- # @param environment [String, nil] Optional environment
397
- # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical)
357
+ # @param environment [String, nil] Optional per-score environment override
358
+ # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction)
398
359
  # @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score
399
360
  # @param config_id [String, nil] Optional score config ID
400
361
  # @return [void]
@@ -408,10 +369,59 @@ module Langfuse
408
369
  #
409
370
  # @example Categorical score
410
371
  # client.create_score(name: "category", value: "high", trace_id: "abc123", data_type: :categorical)
372
+ #
373
+ # @example Text score (1 to 500 characters)
374
+ # client.create_score(name: "reviewer_notes", value: "Helpful but verbose",
375
+ # trace_id: "abc123", data_type: :text)
376
+ #
377
+ # @example Corrected output (conventionally named "output")
378
+ # client.create_score(name: "output", value: "The corrected output", trace_id: "abc123",
379
+ # observation_id: "def456", data_type: :correction)
411
380
  # rubocop:disable Metrics/ParameterLists
412
381
  def create_score(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil,
413
382
  metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil)
414
- @score_client.create(
383
+ active_score_client&.create(
384
+ name: name,
385
+ value: value,
386
+ id: id,
387
+ trace_id: trace_id,
388
+ session_id: session_id,
389
+ observation_id: observation_id,
390
+ comment: comment,
391
+ metadata: metadata,
392
+ environment: environment,
393
+ data_type: data_type,
394
+ dataset_run_id: dataset_run_id,
395
+ config_id: config_id
396
+ )
397
+ end
398
+ # rubocop:enable Metrics/ParameterLists
399
+
400
+ # Create a score immediately through the Scores API. See {ScoreClient#create!}.
401
+ #
402
+ # @param name [String] Score name (required)
403
+ # @param value [Numeric, Integer, String] Score value (type depends on data_type)
404
+ # @param id [String, nil] Score ID; use a stable value as an idempotency key
405
+ # @param trace_id [String, nil] Trace ID to associate with the score
406
+ # @param session_id [String, nil] Session ID to associate with the score
407
+ # @param observation_id [String, nil] Observation ID to associate with the score
408
+ # @param comment [String, nil] Optional comment
409
+ # @param metadata [Hash, nil] Optional metadata hash
410
+ # @param environment [String, nil] Optional per-score environment override
411
+ # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction)
412
+ # @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score
413
+ # @param config_id [String, nil] Optional score config ID
414
+ # @return [String, nil] ID of the created score, or nil when telemetry is disabled
415
+ # @raise [ArgumentError] if validation fails
416
+ # @raise [UnauthorizedError] if authentication fails
417
+ # @raise [ApiError] if the API request fails
418
+ #
419
+ # @example Create a score with an idempotency key
420
+ # client.create_score!(id: "feedback-abc123", name: "quality", value: 0.85, trace_id: "abc123")
421
+ # rubocop:disable Metrics/ParameterLists
422
+ def create_score!(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil,
423
+ metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil)
424
+ active_score_client&.create!(
415
425
  name: name,
416
426
  value: value,
417
427
  id: id,
@@ -436,7 +446,7 @@ module Langfuse
436
446
  # @param value [Numeric, Integer, String] Score value
437
447
  # @param comment [String, nil] Optional comment
438
448
  # @param metadata [Hash, nil] Optional metadata hash
439
- # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical)
449
+ # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction)
440
450
  # @return [void]
441
451
  # @raise [ArgumentError] if no active span or validation fails
442
452
  #
@@ -445,7 +455,7 @@ module Langfuse
445
455
  # client.score_active_observation(name: "accuracy", value: 0.92)
446
456
  # end
447
457
  def score_active_observation(name:, value:, comment: nil, metadata: nil, data_type: :numeric)
448
- @score_client.score_active_observation(
458
+ active_score_client&.score_active_observation(
449
459
  name: name,
450
460
  value: value,
451
461
  comment: comment,
@@ -462,7 +472,7 @@ module Langfuse
462
472
  # @param value [Numeric, Integer, String] Score value
463
473
  # @param comment [String, nil] Optional comment
464
474
  # @param metadata [Hash, nil] Optional metadata hash
465
- # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical)
475
+ # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction)
466
476
  # @return [void]
467
477
  # @raise [ArgumentError] if no active span or validation fails
468
478
  #
@@ -471,7 +481,7 @@ module Langfuse
471
481
  # client.score_active_trace(name: "overall_quality", value: 5)
472
482
  # end
473
483
  def score_active_trace(name:, value:, comment: nil, metadata: nil, data_type: :numeric)
474
- @score_client.score_active_trace(
484
+ active_score_client&.score_active_trace(
475
485
  name: name,
476
486
  value: value,
477
487
  comment: comment,
@@ -489,7 +499,7 @@ module Langfuse
489
499
  # @example
490
500
  # client.flush_scores
491
501
  def flush_scores
492
- @score_client.flush
502
+ active_score_client&.flush
493
503
  end
494
504
 
495
505
  # Shutdown the client and flush any pending scores
@@ -498,8 +508,8 @@ module Langfuse
498
508
  #
499
509
  # @return [void]
500
510
  def shutdown
501
- @score_client.shutdown
502
- @api_client.shutdown
511
+ @score_client&.shutdown
512
+ @api_client_mutex.synchronize { @api_client }&.shutdown
503
513
  end
504
514
 
505
515
  # Create a new dataset
@@ -533,49 +543,6 @@ module Langfuse
533
543
  DatasetClient.new(data, client: self)
534
544
  end
535
545
 
536
- # List all datasets in the project
537
- #
538
- # @param page [Integer, nil] Optional page number for pagination
539
- # @param limit [Integer, nil] Optional limit per page
540
- # @return [Array<Hash>] Array of dataset metadata hashes
541
- # @raise [UnauthorizedError] if authentication fails
542
- # @raise [ApiError] for other API errors
543
- #
544
- # @example
545
- # datasets = client.list_datasets(page: 1, limit: 10)
546
- def list_datasets(page: nil, limit: nil)
547
- api_client.list_datasets(page: page, limit: limit)
548
- end
549
-
550
- # List traces in the project
551
- #
552
- # @param page [Integer, nil] Optional page number for pagination
553
- # @param limit [Integer, nil] Optional limit per page
554
- # @param filters [Hash] Additional filters (user_id, name, session_id, etc.)
555
- # @return [Array<Hash>] Array of trace hashes
556
- # @raise [UnauthorizedError] if authentication fails
557
- # @raise [ApiError] for other API errors
558
- #
559
- # @example
560
- # traces = client.list_traces(page: 1, limit: 10, name: "my-trace")
561
- def list_traces(page: nil, limit: nil, **filters)
562
- api_client.list_traces(page: page, limit: limit, **filters)
563
- end
564
-
565
- # Fetch a trace by ID
566
- #
567
- # @param id [String] Trace ID
568
- # @return [Hash] The trace data
569
- # @raise [NotFoundError] if the trace is not found
570
- # @raise [UnauthorizedError] if authentication fails
571
- # @raise [ApiError] for other API errors
572
- #
573
- # @example
574
- # trace = client.get_trace("trace-uuid-123")
575
- def get_trace(id)
576
- api_client.get_trace(id)
577
- end
578
-
579
546
  # Create a new dataset item
580
547
  #
581
548
  # @param dataset_name [String] Name of the dataset to add item to (required)
@@ -669,39 +636,6 @@ module Langfuse
669
636
  nil
670
637
  end
671
638
 
672
- # Create a dataset run item (link a trace to a dataset item)
673
- #
674
- # @param dataset_item_id [String] Dataset item ID (required)
675
- # @param run_name [String] Run name (required)
676
- # @param trace_id [String, nil] Trace ID
677
- # @param observation_id [String, nil] Observation ID
678
- # @param metadata [Hash, nil] Optional metadata
679
- # @param run_description [String, nil] Optional run description
680
- # @return [Hash] The created dataset run item data
681
- def create_dataset_run_item(dataset_item_id:, run_name:, trace_id: nil,
682
- observation_id: nil, metadata: nil, run_description: nil)
683
- api_client.create_dataset_run_item(
684
- dataset_item_id: dataset_item_id,
685
- run_name: run_name,
686
- trace_id: trace_id,
687
- observation_id: observation_id,
688
- metadata: metadata,
689
- run_description: run_description
690
- )
691
- end
692
-
693
- # Fetch a dataset run by dataset and run name
694
- #
695
- # @param dataset_name [String] Dataset name (required)
696
- # @param run_name [String] Run name (required)
697
- # @return [Hash] The dataset run data, including linked run items
698
- # @raise [NotFoundError] if the dataset run is not found
699
- # @raise [UnauthorizedError] if authentication fails
700
- # @raise [ApiError] for other API errors
701
- def get_dataset_run(dataset_name:, run_name:)
702
- api_client.get_dataset_run(dataset_name: dataset_name, run_name: run_name)
703
- end
704
-
705
639
  # List dataset runs for a dataset
706
640
  #
707
641
  # When page is nil (default), auto-paginates to fetch all runs.
@@ -776,6 +710,38 @@ module Langfuse
776
710
 
777
711
  private
778
712
 
713
+ def build_validated_api_client
714
+ config.validate!
715
+ build_api_client
716
+ end
717
+
718
+ def build_api_client
719
+ cache = create_cache if cache_enabled?
720
+ ApiClient.new(
721
+ public_key: config.public_key,
722
+ secret_key: config.secret_key,
723
+ base_url: config.base_url,
724
+ timeout: config.timeout,
725
+ logger: config.logger,
726
+ cache: cache,
727
+ cache_observer: config.prompt_cache_observer
728
+ )
729
+ end
730
+
731
+ def telemetry_enabled?
732
+ config.telemetry_enabled?
733
+ end
734
+
735
+ def active_score_client
736
+ return unless telemetry_enabled?
737
+
738
+ @score_client_mutex.synchronize { @score_client ||= build_score_client }
739
+ end
740
+
741
+ def build_score_client
742
+ ScoreClient.new(api_client: api_client, config: config)
743
+ end
744
+
779
745
  attr_reader :score_client
780
746
 
781
747
  # Build a project-scoped URL, returning nil if project ID is unavailable
@@ -877,7 +843,7 @@ module Langfuse
877
843
 
878
844
  def fallback_cache_status(cache_ttl)
879
845
  return CacheStatus::BYPASS if cache_ttl&.zero?
880
- return CacheStatus::DISABLED unless api_client.cache
846
+ return CacheStatus::DISABLED unless api_client.prompt_cache_stats[:enabled]
881
847
 
882
848
  CacheStatus::MISS
883
849
  end
@@ -906,7 +872,7 @@ module Langfuse
906
872
  end
907
873
 
908
874
  def rails_cache_available?
909
- defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
875
+ RailsCacheAdapter.available?
910
876
  end
911
877
 
912
878
  # Create in-memory cache with SWR support if enabled
@@ -932,21 +898,22 @@ module Langfuse
932
898
  )
933
899
  end
934
900
 
901
+ VALID_PROMPT_TYPES = %i[text chat].freeze
902
+ private_constant :VALID_PROMPT_TYPES
903
+
935
904
  # Build the appropriate prompt client based on prompt type
936
905
  #
937
906
  # @param prompt_data [Hash] The prompt data from API
938
907
  # @return [TextPromptClient, ChatPromptClient]
939
908
  # @raise [ApiError] if prompt type is unknown
940
- def build_prompt_client(prompt_data)
941
- type = prompt_data["type"]
942
-
943
- case type
909
+ def build_prompt_client(prompt_data, is_fallback: false)
910
+ case prompt_data["type"]
944
911
  when "text"
945
- TextPromptClient.new(prompt_data)
912
+ TextPromptClient.new(prompt_data, is_fallback: is_fallback)
946
913
  when "chat"
947
- ChatPromptClient.new(prompt_data)
914
+ ChatPromptClient.new(prompt_data, is_fallback: is_fallback)
948
915
  else
949
- raise ApiError, "Unknown prompt type: #{type}"
916
+ raise ApiError, "Unknown prompt type: #{prompt_data['type']}"
950
917
  end
951
918
  end
952
919
 
@@ -959,24 +926,18 @@ module Langfuse
959
926
  # @raise [ArgumentError] if type is invalid
960
927
  def build_fallback_prompt_client(name, fallback, type)
961
928
  validate_prompt_type!(type)
962
-
963
- # Create minimal prompt data structure
964
- prompt_data = {
965
- "name" => name,
966
- "version" => 0,
967
- "type" => type.to_s,
968
- "prompt" => fallback,
969
- "labels" => [],
970
- "tags" => ["fallback"],
971
- "config" => {}
972
- }
973
-
974
- case type
975
- when :text
976
- TextPromptClient.new(prompt_data, is_fallback: true)
977
- when :chat
978
- ChatPromptClient.new(prompt_data, is_fallback: true)
979
- end
929
+ build_prompt_client(
930
+ {
931
+ "name" => name,
932
+ "version" => 0,
933
+ "type" => type.to_s,
934
+ "prompt" => fallback,
935
+ "labels" => [],
936
+ "tags" => ["fallback"],
937
+ "config" => {}
938
+ },
939
+ is_fallback: true
940
+ )
980
941
  end
981
942
 
982
943
  # Validate prompt type parameter
@@ -984,8 +945,7 @@ module Langfuse
984
945
  # @param type [Symbol] The type to validate
985
946
  # @raise [ArgumentError] if type is invalid
986
947
  def validate_prompt_type!(type)
987
- valid_types = %i[text chat]
988
- return if valid_types.include?(type)
948
+ return if VALID_PROMPT_TYPES.include?(type)
989
949
 
990
950
  raise ArgumentError, "Invalid type: #{type}. Must be :text or :chat"
991
951
  end
@@ -1015,30 +975,15 @@ module Langfuse
1015
975
  def normalize_prompt_content(prompt, type)
1016
976
  return prompt if type == :text
1017
977
 
1018
- # Normalize chat messages to use string keys
1019
978
  prompt.map do |message|
1020
979
  normalized = message.transform_keys(&:to_s)
1021
- next placeholder_prompt_content(normalized) if normalized["type"] == ChatPromptClient::PLACEHOLDER_TYPE
1022
-
1023
- normalize_chat_message_content(normalized)
980
+ if normalized["type"] == ChatPromptClient::PLACEHOLDER_TYPE
981
+ { "type" => ChatPromptClient::PLACEHOLDER_TYPE, "name" => normalized["name"].to_s }
982
+ else
983
+ normalized.merge("role" => normalized["role"]&.to_s, "content" => normalized["content"])
984
+ end
1024
985
  end
1025
986
  end
1026
-
1027
- # @api private
1028
- def placeholder_prompt_content(message)
1029
- {
1030
- "type" => ChatPromptClient::PLACEHOLDER_TYPE,
1031
- "name" => message["name"].to_s
1032
- }
1033
- end
1034
-
1035
- # @api private
1036
- def normalize_chat_message_content(message)
1037
- message.merge(
1038
- "role" => message["role"]&.to_s,
1039
- "content" => message["content"]
1040
- )
1041
- end
1042
987
  end
1043
988
  # rubocop:enable Metrics/ClassLength
1044
989
  end