44require "faraday/retry"
55require "base64"
66require "json"
7+ require "uri"
78
89module Langfuse
910 # HTTP client for Langfuse API
@@ -128,6 +129,84 @@ def get_prompt(name, version: nil, label: nil)
128129 end
129130 end
130131
132+ # Create a new prompt (or new version if prompt with same name exists)
133+ #
134+ # @param name [String] The prompt name
135+ # @param prompt [String, Array<Hash>] The prompt content
136+ # @param type [String] Prompt type ("text" or "chat")
137+ # @param config [Hash] Optional configuration (model params, etc.)
138+ # @param labels [Array<String>] Optional labels (e.g., ["production"])
139+ # @param tags [Array<String>] Optional tags
140+ # @param commit_message [String, nil] Optional commit message
141+ # @return [Hash] The created prompt data
142+ # @raise [UnauthorizedError] if authentication fails
143+ # @raise [ApiError] for other API errors
144+ #
145+ # @example Create a text prompt
146+ # api_client.create_prompt(
147+ # name: "greeting",
148+ # prompt: "Hello {{name}}!",
149+ # type: "text",
150+ # labels: ["production"]
151+ # )
152+ #
153+ # rubocop:disable Metrics/ParameterLists
154+ def create_prompt ( name :, prompt :, type :, config : { } , labels : [ ] , tags : [ ] , commit_message : nil )
155+ path = "/api/public/v2/prompts"
156+ payload = {
157+ name : name ,
158+ prompt : prompt ,
159+ type : type ,
160+ config : config ,
161+ labels : labels ,
162+ tags : tags
163+ }
164+ payload [ :commitMessage ] = commit_message if commit_message
165+
166+ response = connection . post ( path , payload )
167+ handle_response ( response )
168+ rescue Faraday ::RetriableResponse => e
169+ logger . error ( "Faraday error: Retries exhausted - #{ e . response . status } " )
170+ handle_response ( e . response )
171+ rescue Faraday ::Error => e
172+ logger . error ( "Faraday error: #{ e . message } " )
173+ raise ApiError , "HTTP request failed: #{ e . message } "
174+ end
175+ # rubocop:enable Metrics/ParameterLists
176+
177+ # Update labels for an existing prompt version
178+ #
179+ # @param name [String] The prompt name
180+ # @param version [Integer] The version number to update
181+ # @param labels [Array<String>] New labels (replaces existing). Required.
182+ # @return [Hash] The updated prompt data
183+ # @raise [ArgumentError] if labels is not an array
184+ # @raise [NotFoundError] if the prompt is not found
185+ # @raise [UnauthorizedError] if authentication fails
186+ # @raise [ApiError] for other API errors
187+ #
188+ # @example Promote a prompt to production
189+ # api_client.update_prompt(
190+ # name: "greeting",
191+ # version: 2,
192+ # labels: ["production"]
193+ # )
194+ def update_prompt ( name :, version :, labels :)
195+ raise ArgumentError , "labels must be an array" unless labels . is_a? ( Array )
196+
197+ path = "/api/public/v2/prompts/#{ URI . encode_uri_component ( name ) } /versions/#{ version } "
198+ payload = { newLabels : labels }
199+
200+ response = connection . patch ( path , payload )
201+ handle_response ( response )
202+ rescue Faraday ::RetriableResponse => e
203+ logger . error ( "Faraday error: Retries exhausted - #{ e . response . status } " )
204+ handle_response ( e . response )
205+ rescue Faraday ::Error => e
206+ logger . error ( "Faraday error: #{ e . message } " )
207+ raise ApiError , "HTTP request failed: #{ e . message } "
208+ end
209+
131210 # Send a batch of events to the Langfuse ingestion API
132211 #
133212 # Sends events (scores, traces, observations) to the ingestion endpoint.
@@ -180,7 +259,7 @@ def send_batch(events)
180259 # @raise [ApiError] for other API errors
181260 def fetch_prompt_from_api ( name , version : nil , label : nil )
182261 params = build_prompt_params ( version : version , label : label )
183- path = "/api/public/v2/prompts/#{ name } "
262+ path = "/api/public/v2/prompts/#{ URI . encode_uri_component ( name ) } "
184263
185264 response = connection . get ( path , params )
186265 handle_response ( response )
@@ -215,7 +294,9 @@ def build_connection(timeout: nil)
215294 # Retries transient errors with exponential backoff:
216295 # - Max 2 retries (3 total attempts)
217296 # - Exponential backoff (0.05s * 2^retry_count)
218- # - Retries GET requests and POST requests to batch endpoint (idempotent operations)
297+ # - Retries GET and PATCH requests (idempotent operations)
298+ # - Retries POST requests to batch endpoint (idempotent due to event UUIDs)
299+ # - Note: POST to create_prompt is NOT idempotent; retries may create duplicate versions
219300 # - Retries on: 429 (rate limit), 503 (service unavailable), 504 (gateway timeout)
220301 # - Does NOT retry on: 4xx errors (except 429), 5xx errors (except 503, 504)
221302 #
@@ -225,7 +306,7 @@ def retry_options
225306 max : 2 ,
226307 interval : 0.05 ,
227308 backoff_factor : 2 ,
228- methods : %i[ get post ] ,
309+ methods : %i[ get post patch ] ,
229310 retry_statuses : [ 429 , 503 , 504 ] ,
230311 exceptions : [ Faraday ::TimeoutError , Faraday ::ConnectionFailed ]
231312 }
@@ -278,7 +359,7 @@ def build_prompt_params(version: nil, label: nil)
278359 # @raise [ApiError] for other error statuses
279360 def handle_response ( response )
280361 case response . status
281- when 200
362+ when 200 , 201
282363 response . body
283364 when 401
284365 raise UnauthorizedError , "Authentication failed. Check your API keys."
0 commit comments