Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/langfuse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@
# prompt = client.get_prompt("greeting")
#
module Langfuse
# Base error class for all Langfuse SDK errors
class Error < StandardError; end

# Raised when Langfuse configuration is invalid or incomplete
class ConfigurationError < Error; end

# Raised when a Langfuse API request fails
class ApiError < Error; end

# Raised when a requested resource is not found (HTTP 404)
class NotFoundError < ApiError; end

# Raised when API authentication fails (HTTP 401)
class UnauthorizedError < ApiError; end

# Default timeout (in seconds) for flushing traces during experiment runs.
Expand Down Expand Up @@ -58,6 +67,7 @@ class UnauthorizedError < ApiError; end
module Langfuse
# rubocop:disable Metrics/ClassLength
class << self
# @param configuration [Config] the global configuration object
attr_writer :configuration

# Returns the global configuration object
Expand Down Expand Up @@ -302,6 +312,7 @@ def reset!
# @param start_time [Time, Integer, nil] Optional start time (Time object or Unix timestamp in nanoseconds)
# @param skip_validation [Boolean] Skip validation (for internal use). Defaults to false.
# @return [BaseObservation] The observation wrapper (Span, Generation, or Event)
# @raise [ArgumentError] if an invalid observation type is provided
#
# @example Create root span
# span = Langfuse.start_observation("root-operation", { input: {...} })
Expand Down Expand Up @@ -348,6 +359,7 @@ def start_observation(name, attrs = {}, as_type: :span, parent_span_context: nil
# @param name [String] Descriptive name for the observation
# @param attrs [Hash] Observation attributes (optional positional or keyword)
# @param as_type [Symbol, String] Observation type (:span, :generation, :event, etc.)
# @param kwargs [Hash] Additional keyword arguments merged into observation attributes (e.g., input:, output:, metadata:)
# @yield [observation] Optional block that receives the observation object
# @yieldparam observation [BaseObservation] The observation object
# @return [BaseObservation, Object] The observation (or block return value if block given)
Expand Down
55 changes: 53 additions & 2 deletions lib/langfuse/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,23 @@ module Langfuse
# )
#
class ApiClient # rubocop:disable Metrics/ClassLength
attr_reader :public_key, :secret_key, :base_url, :timeout, :logger, :cache
# @return [String] Langfuse public API key
attr_reader :public_key

# @return [String] Langfuse secret API key
attr_reader :secret_key

# @return [String] Base URL for Langfuse API
attr_reader :base_url

# @return [Integer] HTTP request timeout in seconds
attr_reader :timeout

# @return [Logger] Logger instance for debugging
attr_reader :logger

# @return [PromptCache, RailsCacheAdapter, nil] Optional cache for prompt responses
attr_reader :cache

# Initialize a new API client
#
Expand All @@ -31,7 +47,8 @@ class ApiClient # rubocop:disable Metrics/ClassLength
# @param base_url [String] Base URL for Langfuse API
# @param timeout [Integer] HTTP request timeout in seconds
# @param logger [Logger] Logger instance for debugging
# @param cache [PromptCache, nil] Optional cache for prompt responses
# @param cache [PromptCache, RailsCacheAdapter, nil] Optional cache for prompt responses
# @return [ApiClient]
def initialize(public_key:, secret_key:, base_url:, timeout: 5, logger: nil, cache: nil)
@public_key = public_key
@secret_key = secret_key
Expand Down Expand Up @@ -195,6 +212,7 @@ def update_prompt(name:, version:, labels:)
#
# @param events [Array<Hash>] Array of event hashes to send
# @return [void]
# @raise [ArgumentError] if events is not an Array or is empty
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors after retries exhausted
#
Expand Down Expand Up @@ -237,6 +255,9 @@ def send_batch(events)
# @return [Hash] The created dataset run item data
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# api_client.create_dataset_run_item(dataset_item_id: "item-123", run_name: "eval-v1", trace_id: "trace-abc")
def create_dataset_run_item(dataset_item_id:, run_name:, trace_id: nil,
observation_id: nil, metadata: nil, run_description: nil)
payload = { datasetItemId: dataset_item_id, runName: run_name }
Expand All @@ -255,6 +276,11 @@ def create_dataset_run_item(dataset_item_id:, run_name:, trace_id: nil,
raise ApiError, "HTTP request failed: #{e.message}"
end

# Shut down the API client and release resources
#
# Shuts down the cache if it supports shutdown (e.g., SWR thread pool).
#
# @return [void]
def shutdown
cache.shutdown if cache.respond_to?(:shutdown)
end
Expand All @@ -266,6 +292,9 @@ def shutdown
# @return [Array<Hash>] Array of dataset metadata hashes
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# datasets = api_client.list_datasets(page: 1, limit: 10)
def list_datasets(page: nil, limit: nil)
params = { page: page, limit: limit }.compact

Expand All @@ -287,6 +316,9 @@ def list_datasets(page: nil, limit: nil)
# @raise [NotFoundError] if the dataset is not found
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# data = api_client.get_dataset("my-dataset")
def get_dataset(name)
encoded_name = URI.encode_uri_component(name)
response = connection.get("/api/public/v2/datasets/#{encoded_name}")
Expand All @@ -307,6 +339,9 @@ def get_dataset(name)
# @return [Hash] The created dataset data
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# data = api_client.create_dataset(name: "my-dataset", description: "QA evaluation set")
def create_dataset(name:, description: nil, metadata: nil)
payload = { name: name, description: description, metadata: metadata }.compact

Expand All @@ -333,6 +368,13 @@ def create_dataset(name:, description: nil, metadata: nil)
# @return [Hash] The created dataset item data
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# data = api_client.create_dataset_item(
# dataset_name: "my-dataset",
# input: { query: "What is Ruby?" },
# expected_output: { answer: "A programming language" }
# )
# rubocop:disable Metrics/ParameterLists
def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
metadata: nil, id: nil, source_trace_id: nil,
Expand Down Expand Up @@ -361,6 +403,9 @@ def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
# @raise [NotFoundError] if the item is not found
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# data = api_client.get_dataset_item("item-uuid-123")
def get_dataset_item(id)
encoded_id = URI.encode_uri_component(id)
response = connection.get("/api/public/dataset-items/#{encoded_id}")
Expand All @@ -383,6 +428,9 @@ def get_dataset_item(id)
# @return [Array<Hash>] Array of dataset item hashes
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# items = api_client.list_dataset_items(dataset_name: "my-dataset", limit: 50)
def list_dataset_items(dataset_name:, page: nil, limit: nil,
source_trace_id: nil, source_observation_id: nil)
result = list_dataset_items_paginated(
Expand Down Expand Up @@ -420,6 +468,9 @@ def list_dataset_items_paginated(dataset_name:, page: nil, limit: nil,
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
# @note 404 responses are treated as success to keep DELETE idempotent across retries
#
# @example
# api_client.delete_dataset_item("item-uuid-123")
def delete_dataset_item(id)
encoded_id = URI.encode_uri_component(id)
response = connection.delete("/api/public/dataset-items/#{encoded_id}")
Expand Down
13 changes: 7 additions & 6 deletions lib/langfuse/cache_warmer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Langfuse
# end
#
class CacheWarmer
# @return [Client] Langfuse client used for fetching prompts
attr_reader :client

# Initialize a new cache warmer
Expand All @@ -35,8 +36,8 @@ def initialize(client: nil)
# safe to call multiple times.
#
# @param prompt_names [Array<String>] List of prompt names to cache
# @param versions [Hash<String, Integer>, nil] Optional version numbers per prompt
# @param labels [Hash<String, String>, nil] Optional labels per prompt
# @param versions [Hash<String, Integer>] Optional version numbers per prompt
# @param labels [Hash<String, String>] Optional labels per prompt
# @return [Hash] Results with :success and :failed arrays
#
# @example Basic warming
Expand Down Expand Up @@ -73,8 +74,8 @@ def warm(prompt_names, versions: {}, labels: {})
# are cached without manually specifying them.
#
# @param default_label [String, nil] Label to use for all prompts (default: "production")
# @param versions [Hash<String, Integer>, nil] Optional version numbers per prompt
# @param labels [Hash<String, String>, nil] Optional labels per specific prompts (overrides default_label)
# @param versions [Hash<String, Integer>] Optional version numbers per prompt
# @param labels [Hash<String, String>] Optional labels per specific prompts (overrides default_label)
# @return [Hash] Results with :success and :failed arrays
#
# @example Auto-discover and warm all prompts with "production" label
Expand Down Expand Up @@ -119,8 +120,8 @@ def warm_all(default_label: "production", versions: {}, labels: {})
# Useful when you want to abort deployment if cache warming fails.
#
# @param prompt_names [Array<String>] List of prompt names to cache
# @param versions [Hash<String, Integer>, nil] Optional version numbers per prompt
# @param labels [Hash<String, String>, nil] Optional labels per prompt
# @param versions [Hash<String, Integer>] Optional version numbers per prompt
# @param labels [Hash<String, String>] Optional labels per prompt
# @return [Hash] Results with :success array
# @raise [CacheWarmingError] if any prompts fail to cache
#
Expand Down
18 changes: 17 additions & 1 deletion lib/langfuse/chat_prompt_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ module Langfuse
# chat_prompt.labels # => ["production"]
#
class ChatPromptClient
attr_reader :name, :version, :labels, :tags, :config, :prompt
# @return [String] Prompt name
attr_reader :name

# @return [Integer] Prompt version number
attr_reader :version

# @return [Array<String>] Labels assigned to this prompt
attr_reader :labels

# @return [Array<String>] Tags assigned to this prompt
attr_reader :tags

# @return [Hash] Prompt configuration
attr_reader :config

# @return [Array<Hash>] Array of message hashes with role and content
attr_reader :prompt

# Initialize a new chat prompt client
#
Expand Down
33 changes: 32 additions & 1 deletion lib/langfuse/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ module Langfuse
#
# rubocop:disable Metrics/ClassLength
class Client
# @return [Integer] Default page size when fetching all dataset items
DATASET_ITEMS_PAGE_SIZE = 50

attr_reader :config, :api_client
# @return [Config] The client configuration
attr_reader :config

# @return [ApiClient] The underlying API client
attr_reader :api_client

# Initialize a new Langfuse client
#
# @param config [Config] Configuration object
# @return [Client]
def initialize(config)
@config = config
@config.validate!
Expand Down Expand Up @@ -362,6 +368,9 @@ def shutdown
# @return [DatasetClient] The created dataset client
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# dataset = client.create_dataset(name: "my-dataset", description: "QA evaluation set")
def create_dataset(name:, description: nil, metadata: nil)
data = api_client.create_dataset(name: name, description: description, metadata: metadata)
DatasetClient.new(data, client: self)
Expand All @@ -374,6 +383,9 @@ def create_dataset(name:, description: nil, metadata: nil)
# @raise [NotFoundError] if the dataset is not found
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# dataset = client.get_dataset("my-dataset")
def get_dataset(name)
data = api_client.get_dataset(name)
DatasetClient.new(data, client: self)
Expand All @@ -386,6 +398,9 @@ def get_dataset(name)
# @return [Array<Hash>] Array of dataset metadata hashes
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# datasets = client.list_datasets(page: 1, limit: 10)
def list_datasets(page: nil, limit: nil)
api_client.list_datasets(page: page, limit: limit)
end
Expand All @@ -403,6 +418,13 @@ def list_datasets(page: nil, limit: nil)
# @return [DatasetItemClient] The created dataset item client
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# item = client.create_dataset_item(
# dataset_name: "my-dataset",
# input: { query: "What is Ruby?" },
# expected_output: { answer: "A programming language" }
# )
# rubocop:disable Metrics/ParameterLists
def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
metadata: nil, id: nil, source_trace_id: nil,
Expand All @@ -423,6 +445,9 @@ def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
# @raise [NotFoundError] if the item is not found
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# item = client.get_dataset_item("item-uuid-123")
def get_dataset_item(id)
data = api_client.get_dataset_item(id)
DatasetItemClient.new(data, client: self)
Expand All @@ -441,6 +466,9 @@ def get_dataset_item(id)
# @return [Array<DatasetItemClient>] Array of dataset item clients
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
#
# @example
# items = client.list_dataset_items(dataset_name: "my-dataset", limit: 50)
def list_dataset_items(dataset_name:, page: nil, limit: nil,
source_trace_id: nil, source_observation_id: nil)
filters = { dataset_name: dataset_name, source_trace_id: source_trace_id,
Expand All @@ -462,6 +490,9 @@ def list_dataset_items(dataset_name:, page: nil, limit: nil,
# @raise [UnauthorizedError] if authentication fails
# @raise [ApiError] for other API errors
# @note 404 responses are treated as success to keep DELETE idempotent across retries
#
# @example
# client.delete_dataset_item("item-uuid-123")
def delete_dataset_item(id)
api_client.delete_dataset_item(id)
nil
Expand Down
27 changes: 25 additions & 2 deletions lib/langfuse/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,50 @@ class Config
# @return [Symbol] ActiveJob queue name for async processing
attr_accessor :job_queue

# Default values
# @return [String] Default Langfuse API base URL
DEFAULT_BASE_URL = "https://cloud.langfuse.com"

# @return [Integer] Default HTTP request timeout in seconds
DEFAULT_TIMEOUT = 5

# @return [Integer] Default cache TTL in seconds
DEFAULT_CACHE_TTL = 60

# @return [Integer] Default maximum number of cached items
DEFAULT_CACHE_MAX_SIZE = 1000

# @return [Symbol] Default cache backend
DEFAULT_CACHE_BACKEND = :memory

# @return [Integer] Default lock timeout in seconds for cache stampede protection
DEFAULT_CACHE_LOCK_TIMEOUT = 10

# @return [Boolean] Default stale-while-revalidate setting
DEFAULT_CACHE_STALE_WHILE_REVALIDATE = false

# @return [Integer] Default number of background threads for cache refresh
DEFAULT_CACHE_REFRESH_THREADS = 5

# @return [Boolean] Default async processing setting
DEFAULT_TRACING_ASYNC = true

# @return [Integer] Default number of events to batch before sending
DEFAULT_BATCH_SIZE = 50

# @return [Integer] Default flush interval in seconds
DEFAULT_FLUSH_INTERVAL = 10

# @return [Symbol] Default ActiveJob queue name
DEFAULT_JOB_QUEUE = :default

# Number of seconds representing indefinite cache duration (~1000 years)
# @return [Integer] Number of seconds representing indefinite cache duration (~1000 years)
INDEFINITE_SECONDS = 1000 * 365 * 24 * 60 * 60

# Initialize a new Config object
#
# @yield [config] Optional block for configuration
# @yieldparam config [Config] The config instance
# @return [Config] a new Config instance
def initialize
@public_key = ENV.fetch("LANGFUSE_PUBLIC_KEY", nil)
@secret_key = ENV.fetch("LANGFUSE_SECRET_KEY", nil)
Expand Down
Loading