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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -1
- data/lib/langfuse/api_client.rb +175 -509
- data/lib/langfuse/app_root_tracking.rb +164 -0
- data/lib/langfuse/cache_warmer.rb +21 -13
- data/lib/langfuse/chat_prompt_client.rb +21 -4
- data/lib/langfuse/client.rb +174 -229
- data/lib/langfuse/config.rb +308 -65
- data/lib/langfuse/evaluation.rb +8 -4
- data/lib/langfuse/exit_hook.rb +77 -0
- data/lib/langfuse/fork_safety.rb +71 -0
- data/lib/langfuse/masking_exporter.rb +98 -0
- data/lib/langfuse/observations.rb +2 -1
- data/lib/langfuse/otel_attributes.rb +1 -0
- data/lib/langfuse/otel_setup.rb +32 -44
- data/lib/langfuse/otel_span_batch.rb +113 -0
- data/lib/langfuse/otel_span_masking.rb +89 -0
- data/lib/langfuse/otel_span_patch_applier.rb +97 -0
- data/lib/langfuse/pending_score_queue.rb +62 -0
- data/lib/langfuse/prompt_cache.rb +11 -0
- data/lib/langfuse/prompt_cache_coordinator.rb +288 -0
- data/lib/langfuse/prompt_cache_events.rb +31 -10
- data/lib/langfuse/prompt_variables.rb +54 -0
- data/lib/langfuse/propagation.rb +101 -34
- data/lib/langfuse/rails_cache_adapter.rb +27 -2
- data/lib/langfuse/read_api.rb +242 -0
- data/lib/langfuse/resilient_metrics_reporter.rb +60 -0
- data/lib/langfuse/score_client.rb +209 -89
- data/lib/langfuse/score_value.rb +58 -0
- data/lib/langfuse/span_processor.rb +53 -4
- data/lib/langfuse/stale_while_revalidate.rb +3 -4
- data/lib/langfuse/text_prompt_client.rb +15 -6
- data/lib/langfuse/trace_export_guard.rb +47 -0
- data/lib/langfuse/traced_execution.rb +18 -12
- data/lib/langfuse/types.rb +15 -1
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +157 -44
- metadata +16 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Langfuse
|
|
4
|
+
# Owns application-root state for active span trees.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module AppRootTracking
|
|
8
|
+
APP_ROOT_INELIGIBLE_SPANS = [
|
|
9
|
+
%w[litellm raw_gen_ai_request]
|
|
10
|
+
].freeze
|
|
11
|
+
private_constant :APP_ROOT_INELIGIBLE_SPANS
|
|
12
|
+
|
|
13
|
+
# Return whether an exported span can be an application root when its parent is untracked.
|
|
14
|
+
#
|
|
15
|
+
# LiteLLM can emit `raw_gen_ai_request` after its exported parent finishes.
|
|
16
|
+
# The late child must not become a second application root.
|
|
17
|
+
#
|
|
18
|
+
# @param span [OpenTelemetry::SDK::Trace::Span] Span to inspect
|
|
19
|
+
# @return [Boolean] Whether the span can be an application root
|
|
20
|
+
# @api private
|
|
21
|
+
def self.eligible_without_tracked_parent?(span)
|
|
22
|
+
return true if span.parent_span_id == OpenTelemetry::Trace::INVALID_SPAN_ID
|
|
23
|
+
|
|
24
|
+
identity = [span.instrumentation_scope&.name, span.name]
|
|
25
|
+
!APP_ROOT_INELIGIBLE_SPANS.include?(identity)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Defers a finished span until its active ancestors have final export decisions.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
class Tracker
|
|
32
|
+
ReadySpan = Struct.new(:span, :app_root, keyword_init: true)
|
|
33
|
+
private_constant :ReadySpan
|
|
34
|
+
|
|
35
|
+
State = Struct.new(
|
|
36
|
+
:span,
|
|
37
|
+
:trace_claimed,
|
|
38
|
+
:untracked_parent_root_eligible,
|
|
39
|
+
:parent_span_id,
|
|
40
|
+
:active_child_count,
|
|
41
|
+
:finished,
|
|
42
|
+
:exportable,
|
|
43
|
+
:enqueued,
|
|
44
|
+
keyword_init: true
|
|
45
|
+
)
|
|
46
|
+
private_constant :State
|
|
47
|
+
|
|
48
|
+
def initialize
|
|
49
|
+
@mutex = Mutex.new
|
|
50
|
+
@state_by_span_id = {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param span [OpenTelemetry::SDK::Trace::Span] The active span
|
|
54
|
+
# @param trace_claimed [Boolean] Whether propagated context already owns the root
|
|
55
|
+
# @param untracked_parent_root_eligible [Boolean] Whether an untracked parent permits a root
|
|
56
|
+
# @return [void]
|
|
57
|
+
def remember(span, trace_claimed:, untracked_parent_root_eligible:)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
parent_state = @state_by_span_id[span.parent_span_id]
|
|
60
|
+
parent_state.active_child_count += 1 if parent_state
|
|
61
|
+
@state_by_span_id[span.context.span_id] = build_state(
|
|
62
|
+
span,
|
|
63
|
+
trace_claimed: trace_claimed,
|
|
64
|
+
untracked_parent_root_eligible: untracked_parent_root_eligible
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Resolve finished spans whose ancestor export decisions are final.
|
|
70
|
+
#
|
|
71
|
+
# @param span [OpenTelemetry::SDK::Trace::Span] The finished span
|
|
72
|
+
# @param exportable [Boolean] Whether the final export filter accepted the span
|
|
73
|
+
# @return [Array<ReadySpan>] Spans that the batch processor can enqueue
|
|
74
|
+
def finish(span, exportable:)
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
state = @state_by_span_id[span.context.span_id]
|
|
77
|
+
return [] unless state
|
|
78
|
+
|
|
79
|
+
state.finished = true
|
|
80
|
+
state.exportable = exportable
|
|
81
|
+
ready_spans = resolve_ready_spans
|
|
82
|
+
release_finished_states
|
|
83
|
+
ready_spans
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] Whether the tracker has no active span trees
|
|
88
|
+
def empty?
|
|
89
|
+
@mutex.synchronize { @state_by_span_id.empty? }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def build_state(span, trace_claimed:, untracked_parent_root_eligible:)
|
|
95
|
+
State.new(
|
|
96
|
+
span: span,
|
|
97
|
+
trace_claimed: trace_claimed,
|
|
98
|
+
untracked_parent_root_eligible: untracked_parent_root_eligible,
|
|
99
|
+
parent_span_id: span.parent_span_id,
|
|
100
|
+
active_child_count: 0,
|
|
101
|
+
finished: false,
|
|
102
|
+
exportable: nil,
|
|
103
|
+
enqueued: false
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def resolve_ready_spans
|
|
108
|
+
@state_by_span_id.values.filter_map do |state|
|
|
109
|
+
next unless state.finished && state.exportable && !state.enqueued
|
|
110
|
+
|
|
111
|
+
app_root = app_root_status(state)
|
|
112
|
+
next if app_root.nil?
|
|
113
|
+
|
|
114
|
+
state.enqueued = true
|
|
115
|
+
ReadySpan.new(span: state.span, app_root: app_root)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def app_root_status(state)
|
|
120
|
+
trace_claimed = state.trace_claimed
|
|
121
|
+
parent_span_id = state.parent_span_id
|
|
122
|
+
parent_state = @state_by_span_id[parent_span_id]
|
|
123
|
+
return false unless parent_state || state.untracked_parent_root_eligible
|
|
124
|
+
|
|
125
|
+
while parent_state
|
|
126
|
+
return nil unless parent_state.finished
|
|
127
|
+
return false if parent_state.exportable
|
|
128
|
+
|
|
129
|
+
trace_claimed = parent_state.trace_claimed
|
|
130
|
+
parent_span_id = parent_state.parent_span_id
|
|
131
|
+
parent_state = @state_by_span_id[parent_span_id]
|
|
132
|
+
end
|
|
133
|
+
!trace_claimed
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def release_finished_states
|
|
137
|
+
releasable_span_ids = @state_by_span_id.filter_map do |span_id, state|
|
|
138
|
+
span_id if releasable?(state)
|
|
139
|
+
end
|
|
140
|
+
until releasable_span_ids.empty?
|
|
141
|
+
span_id = releasable_span_ids.pop
|
|
142
|
+
state = @state_by_span_id[span_id]
|
|
143
|
+
next unless releasable?(state)
|
|
144
|
+
|
|
145
|
+
parent_span_id = release_state(span_id, state)
|
|
146
|
+
releasable_span_ids << parent_span_id if parent_span_id
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def releasable?(state)
|
|
151
|
+
state&.finished && state.active_child_count.zero? && (!state.exportable || state.enqueued)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def release_state(span_id, state)
|
|
155
|
+
@state_by_span_id.delete(span_id)
|
|
156
|
+
parent_state = @state_by_span_id[state.parent_span_id]
|
|
157
|
+
return unless parent_state
|
|
158
|
+
|
|
159
|
+
parent_state.active_child_count -= 1
|
|
160
|
+
state.parent_span_id
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -148,25 +148,22 @@ module Langfuse
|
|
|
148
148
|
#
|
|
149
149
|
# @return [Boolean]
|
|
150
150
|
def cache_enabled?
|
|
151
|
-
|
|
152
|
-
return false if cache.nil?
|
|
153
|
-
|
|
154
|
-
cache.ttl&.positive? || false
|
|
151
|
+
client.prompt_cache_stats[:enabled] == true
|
|
155
152
|
end
|
|
156
153
|
|
|
157
154
|
# Get cache statistics (if supported by backend)
|
|
158
155
|
#
|
|
159
156
|
# @return [Hash, nil] Cache stats or nil if not supported
|
|
160
157
|
def cache_stats
|
|
161
|
-
|
|
162
|
-
return nil unless
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
158
|
+
stats = client.prompt_cache_stats
|
|
159
|
+
return nil unless stats[:enabled]
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
backend: public_backend_name(stats[:backend]),
|
|
163
|
+
ttl: stats[:ttl],
|
|
164
|
+
size: stats[:size],
|
|
165
|
+
max_size: stats[:max_size]
|
|
166
|
+
}
|
|
170
167
|
end
|
|
171
168
|
|
|
172
169
|
private
|
|
@@ -213,6 +210,17 @@ module Langfuse
|
|
|
213
210
|
def record_failure(results, name, error)
|
|
214
211
|
results[:failed] << { name: name, error: error }
|
|
215
212
|
end
|
|
213
|
+
|
|
214
|
+
def public_backend_name(backend)
|
|
215
|
+
case backend
|
|
216
|
+
when CacheBackend::MEMORY
|
|
217
|
+
"PromptCache"
|
|
218
|
+
when CacheBackend::RAILS
|
|
219
|
+
"RailsCacheAdapter"
|
|
220
|
+
else
|
|
221
|
+
backend.to_s
|
|
222
|
+
end
|
|
223
|
+
end
|
|
216
224
|
end
|
|
217
225
|
|
|
218
226
|
# Error raised when cache warming fails with warm!
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "prompt_renderer"
|
|
4
|
+
require_relative "prompt_variables"
|
|
4
5
|
|
|
5
6
|
module Langfuse
|
|
6
7
|
# Chat prompt client for compiling chat prompts with variable substitution
|
|
@@ -11,7 +12,7 @@ module Langfuse
|
|
|
11
12
|
# @example Basic usage
|
|
12
13
|
# prompt_data = api_client.get_prompt("support_chat")
|
|
13
14
|
# chat_prompt = Langfuse::ChatPromptClient.new(prompt_data)
|
|
14
|
-
# chat_prompt.compile(
|
|
15
|
+
# chat_prompt.compile(user_name: "Alice", issue: "login")
|
|
15
16
|
# # => [{ role: "system", content: "You are a support agent..." }, ...]
|
|
16
17
|
#
|
|
17
18
|
# @example Accessing metadata
|
|
@@ -28,6 +29,9 @@ module Langfuse
|
|
|
28
29
|
# @return [Integer] Prompt version number
|
|
29
30
|
attr_reader :version
|
|
30
31
|
|
|
32
|
+
# @return [Array<Hash>] Raw prompt template (array of role/content message hashes)
|
|
33
|
+
attr_reader :prompt
|
|
34
|
+
|
|
31
35
|
# @return [Array<String>] Labels assigned to this prompt
|
|
32
36
|
attr_reader :labels
|
|
33
37
|
|
|
@@ -37,9 +41,6 @@ module Langfuse
|
|
|
37
41
|
# @return [Hash] Prompt configuration
|
|
38
42
|
attr_reader :config
|
|
39
43
|
|
|
40
|
-
# @return [Array<Hash>] Array of message hashes and placeholder entries
|
|
41
|
-
attr_reader :prompt
|
|
42
|
-
|
|
43
44
|
# @return [String, nil] Optional commit message for this prompt version
|
|
44
45
|
attr_reader :commit_message
|
|
45
46
|
|
|
@@ -73,6 +74,22 @@ module Langfuse
|
|
|
73
74
|
"chat"
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
# Return the unique variables referenced by all message templates
|
|
78
|
+
#
|
|
79
|
+
# Section names are included because callers must provide their values.
|
|
80
|
+
# Message placeholder entries are not Mustache templates and are excluded.
|
|
81
|
+
#
|
|
82
|
+
# @return [Array<String>] Referenced variable names in message and source order
|
|
83
|
+
# @raise [Mustache::Parser::SyntaxError] if a message contains invalid Mustache syntax
|
|
84
|
+
def variables
|
|
85
|
+
prompt.each_with_object([]) do |message, names|
|
|
86
|
+
normalized = symbolize_keys(message)
|
|
87
|
+
next if normalized[:type].to_s == PLACEHOLDER_TYPE
|
|
88
|
+
|
|
89
|
+
names.concat(PromptVariables.extract(normalized[:content] || ""))
|
|
90
|
+
end.uniq
|
|
91
|
+
end
|
|
92
|
+
|
|
76
93
|
# Compile the chat prompt with variable substitution and message placeholders
|
|
77
94
|
#
|
|
78
95
|
# Returns an array of message hashes with roles and compiled content.
|