From 099b937a70485305d17134e6ddcb24e65e11d3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Wed, 1 Oct 2025 20:04:59 +0200 Subject: [PATCH 1/4] Fetch Langfuse prompt for assistant instructions --- app/models/assistant.rb | 13 ++++-- app/models/assistant/configurable.rb | 68 +++++++++++++++++++++++++++- app/models/assistant/responder.rb | 6 ++- app/models/provider/llm_concept.rb | 1 + app/models/provider/openai.rb | 60 +++++++++++++++--------- 5 files changed, 121 insertions(+), 27 deletions(-) diff --git a/app/models/assistant.rb b/app/models/assistant.rb index c077c2f0d94..7be7d5aa5db 100644 --- a/app/models/assistant.rb +++ b/app/models/assistant.rb @@ -1,18 +1,24 @@ class Assistant include Provided, Configurable, Broadcastable - attr_reader :chat, :instructions + attr_reader :chat, :instructions, :instructions_prompt class << self def for_chat(chat) config = config_for(chat) - new(chat, instructions: config[:instructions], functions: config[:functions]) + new( + chat, + instructions: config[:instructions], + instructions_prompt: config[:instructions_prompt], + functions: config[:functions] + ) end end - def initialize(chat, instructions: nil, functions: []) + def initialize(chat, instructions: nil, instructions_prompt: nil, functions: []) @chat = chat @instructions = instructions + @instructions_prompt = instructions_prompt @functions = functions end @@ -26,6 +32,7 @@ def respond_to(message) responder = Assistant::Responder.new( message: message, instructions: instructions, + instructions_prompt: instructions_prompt, function_tool_caller: function_tool_caller, llm: get_model_provider(message.ai_model) ) diff --git a/app/models/assistant/configurable.rb b/app/models/assistant/configurable.rb index 1da95d14b87..d6cce302ae3 100644 --- a/app/models/assistant/configurable.rb +++ b/app/models/assistant/configurable.rb @@ -6,8 +6,11 @@ def config_for(chat) preferred_currency = Money::Currency.new(chat.user.family.currency) preferred_date_format = chat.user.family.date_format + instructions_config = default_instructions(preferred_currency, preferred_date_format) + { - instructions: default_instructions(preferred_currency, preferred_date_format), + instructions: instructions_config[:content], + instructions_prompt: instructions_config[:prompt], functions: default_functions } end @@ -23,6 +26,60 @@ def default_functions end def default_instructions(preferred_currency, preferred_date_format) + langfuse_instructions = langfuse_default_instructions(preferred_currency, preferred_date_format) + + if langfuse_instructions.present? + { + content: langfuse_instructions[:content], + prompt: langfuse_instructions + } + else + { + content: fallback_default_instructions(preferred_currency, preferred_date_format), + prompt: nil + } + end + end + + def langfuse_default_instructions(preferred_currency, preferred_date_format) + return unless langfuse_client + + prompt = langfuse_client.get_prompt("default_instructions") + + compiled_prompt = prompt.compile( + preferred_currency_symbol: preferred_currency.symbol, + preferred_currency_iso_code: preferred_currency.iso_code, + preferred_currency_default_precision: preferred_currency.default_precision, + preferred_currency_default_format: preferred_currency.default_format, + preferred_currency_separator: preferred_currency.separator, + preferred_currency_delimiter: preferred_currency.delimiter, + preferred_date_format: preferred_date_format, + current_date: Date.current + ) + + content = case compiled_prompt + when String + compiled_prompt + when Array + compiled_prompt.filter_map { |message| message[:content] }.join("\n\n") + else + nil + end + + return if content.blank? + + { + name: prompt.name, + version: prompt.version, + template: prompt.prompt, + content: content + } + rescue => e + Rails.logger.warn("Langfuse prompt retrieval failed: #{e.message}") + nil + end + + def fallback_default_instructions(preferred_currency, preferred_date_format) <<~PROMPT ## Your identity @@ -78,5 +135,14 @@ def default_instructions(preferred_currency, preferred_date_format) the data you're presenting represents and what context it is in (i.e. date range, account, etc.) PROMPT end + + def langfuse_client + return unless ENV["LANGFUSE_PUBLIC_KEY"].present? && ENV["LANGFUSE_SECRET_KEY"].present? + + @langfuse_client ||= Langfuse.new + rescue => e + Rails.logger.warn("Langfuse client initialization failed: #{e.message}") + nil + end end end diff --git a/app/models/assistant/responder.rb b/app/models/assistant/responder.rb index dffcf4dd0e5..516c1ea1de1 100644 --- a/app/models/assistant/responder.rb +++ b/app/models/assistant/responder.rb @@ -1,9 +1,10 @@ class Assistant::Responder - def initialize(message:, instructions:, function_tool_caller:, llm:) + def initialize(message:, instructions:, function_tool_caller:, llm:, instructions_prompt: nil) @message = message @instructions = instructions @function_tool_caller = function_tool_caller @llm = llm + @instructions_prompt = instructions_prompt end def on(event_name, &block) @@ -31,7 +32,7 @@ def respond(previous_response_id: nil) end private - attr_reader :message, :instructions, :function_tool_caller, :llm + attr_reader :message, :instructions, :function_tool_caller, :llm, :instructions_prompt def handle_follow_up_response(response) streamer = proc do |chunk| @@ -64,6 +65,7 @@ def get_llm_response(streamer:, function_results: [], previous_response_id: nil) message.content, model: message.ai_model, instructions: instructions, + instructions_prompt: instructions_prompt, functions: function_tool_caller.function_definitions, function_results: function_results, streamer: streamer, diff --git a/app/models/provider/llm_concept.rb b/app/models/provider/llm_concept.rb index be0b46a07ed..bf9e2251610 100644 --- a/app/models/provider/llm_concept.rb +++ b/app/models/provider/llm_concept.rb @@ -22,6 +22,7 @@ def chat_response( prompt, model:, instructions: nil, + instructions_prompt: nil, functions: [], function_results: [], streamer: nil, diff --git a/app/models/provider/openai.rb b/app/models/provider/openai.rb index ddde08bad4c..75bff06e1a7 100644 --- a/app/models/provider/openai.rb +++ b/app/models/provider/openai.rb @@ -62,6 +62,7 @@ def chat_response( prompt, model:, instructions: nil, + instructions_prompt: nil, functions: [], function_results: [], streamer: nil, @@ -112,6 +113,7 @@ def chat_response( model: model, input: input_payload, output: response.messages.map(&:output_text).join("\n"), + prompt: instructions_prompt, session_id: session_id, user_identifier: user_identifier ) @@ -123,6 +125,7 @@ def chat_response( model: model, input: input_payload, output: parsed.messages.map(&:output_text).join("\n"), + prompt: instructions_prompt, usage: raw_response["usage"], session_id: session_id, user_identifier: user_identifier @@ -141,26 +144,41 @@ def langfuse_client @langfuse_client = Langfuse.new end - def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_id: nil, user_identifier: nil) - return unless langfuse_client - - trace = langfuse_client.trace( - name: "openai.#{name}", - input: input, - session_id: session_id, - user_id: user_identifier - ) - trace.generation( - name: name, - model: model, - input: input, - output: output, - usage: usage, - session_id: session_id, - user_id: user_identifier - ) - trace.update(output: output) - rescue => e - Rails.logger.warn("Langfuse logging failed: #{e.message}") + def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_id: nil, user_identifier: nil, prompt: nil) + return unless langfuse_client + + trace = langfuse_client.trace( + name: "openai.#{name}", + input: input, + session_id: session_id, + user_id: user_identifier + ) + generation_options = { + name: name, + model: model, + input: input, + output: output, + usage: usage, + session_id: session_id, + user_id: user_identifier + } + + if prompt.present? + generation_options[:version] = prompt[:version] if prompt[:version] + metadata = { + prompt: { + name: prompt[:name], + version: prompt[:version], + content: prompt[:content], + template: prompt[:template] + }.compact + } + generation_options[:metadata] = metadata end + + trace.generation(**generation_options) + trace.update(output: output) + rescue => e + Rails.logger.warn("Langfuse logging failed: #{e.message}") + end end From d8804ea78bf0b595b23687a3f68c0008818b70ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Wed, 1 Oct 2025 20:45:07 +0200 Subject: [PATCH 2/4] Handle nil values when compiling Langfuse instructions --- app/models/assistant/configurable.rb | 53 ++++++++++++++++++---------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/app/models/assistant/configurable.rb b/app/models/assistant/configurable.rb index d6cce302ae3..ed794e6ded3 100644 --- a/app/models/assistant/configurable.rb +++ b/app/models/assistant/configurable.rb @@ -45,27 +45,14 @@ def langfuse_default_instructions(preferred_currency, preferred_date_format) return unless langfuse_client prompt = langfuse_client.get_prompt("default_instructions") - - compiled_prompt = prompt.compile( - preferred_currency_symbol: preferred_currency.symbol, - preferred_currency_iso_code: preferred_currency.iso_code, - preferred_currency_default_precision: preferred_currency.default_precision, - preferred_currency_default_format: preferred_currency.default_format, - preferred_currency_separator: preferred_currency.separator, - preferred_currency_delimiter: preferred_currency.delimiter, - preferred_date_format: preferred_date_format, - current_date: Date.current + return if prompt.nil? + compiled_prompt = compile_langfuse_prompt( + prompt, + preferred_currency: preferred_currency, + preferred_date_format: preferred_date_format ) - content = case compiled_prompt - when String - compiled_prompt - when Array - compiled_prompt.filter_map { |message| message[:content] }.join("\n\n") - else - nil - end - + content = extract_prompt_content(compiled_prompt) return if content.blank? { @@ -79,6 +66,34 @@ def langfuse_default_instructions(preferred_currency, preferred_date_format) nil end + def compile_langfuse_prompt(prompt, preferred_currency:, preferred_date_format:) + variables = { + preferred_currency_symbol: preferred_currency&.symbol, + preferred_currency_iso_code: preferred_currency&.iso_code, + preferred_currency_default_precision: preferred_currency&.default_precision, + preferred_currency_default_format: preferred_currency&.default_format, + preferred_currency_separator: preferred_currency&.separator, + preferred_currency_delimiter: preferred_currency&.delimiter, + preferred_date_format: preferred_date_format, + current_date: Date.current + }.transform_values { |value| value.nil? ? "" : value.to_s } + + prompt.compile(**variables) + end + + def extract_prompt_content(compiled_prompt) + case compiled_prompt + when String + compiled_prompt + when Array + compiled_prompt.filter_map do |message| + message[:content] || message["content"] + end.join("\n\n") + else + nil + end + end + def fallback_default_instructions(preferred_currency, preferred_date_format) <<~PROMPT ## Your identity From 17f5c027d82bdb5d21fd64d7f36a704e63e39b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Wed, 1 Oct 2025 19:47:19 +0000 Subject: [PATCH 3/4] Fixes and prompt support --- app/models/assistant/configurable.rb | 33 ++++++++++++- app/models/provider/openai.rb | 74 +++++++++++++++------------- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/app/models/assistant/configurable.rb b/app/models/assistant/configurable.rb index ed794e6ded3..1fdca9a1ce6 100644 --- a/app/models/assistant/configurable.rb +++ b/app/models/assistant/configurable.rb @@ -46,8 +46,13 @@ def langfuse_default_instructions(preferred_currency, preferred_date_format) prompt = langfuse_client.get_prompt("default_instructions") return if prompt.nil? + + # TODO: remove after we make the code resilient to chat vs. text types of prompts + Rails.logger.warn("Langfuse prompt retrieved: #{prompt.name} #{prompt.version}") + Rails.logger.warn("Langfuse prompt retrieved: #{prompt.prompt}") + compiled_prompt = compile_langfuse_prompt( - prompt, + prompt.prompt.dig(0, "content"), preferred_currency: preferred_currency, preferred_date_format: preferred_date_format ) @@ -56,6 +61,7 @@ def langfuse_default_instructions(preferred_currency, preferred_date_format) return if content.blank? { + id: prompt.respond_to?(:id) ? prompt.id : (prompt[:id] rescue nil), name: prompt.name, version: prompt.version, template: prompt.prompt, @@ -78,7 +84,30 @@ def compile_langfuse_prompt(prompt, preferred_currency:, preferred_date_format:) current_date: Date.current }.transform_values { |value| value.nil? ? "" : value.to_s } - prompt.compile(**variables) + # If the prompt object supports compilation, use it. Otherwise, perform + # a lightweight local interpolation for String/Array/Hash templates. + if prompt.respond_to?(:compile) + prompt.compile(**variables) + else + interpolate_template(prompt, variables) + end + end + + def interpolate_template(template, variables) + case template + when String + # Replace {{ variable }} placeholders with provided variables + template.gsub(/\{\{\s*(\w+)\s*\}\}/) do + key = Regexp.last_match(1) + variables[key] || "" + end + when Array + template.map { |item| interpolate_template(item, variables) } + when Hash + template.transform_values { |v| interpolate_template(v, variables) } + else + template + end end def extract_prompt_content(compiled_prompt) diff --git a/app/models/provider/openai.rb b/app/models/provider/openai.rb index 75bff06e1a7..58a6ef94a3d 100644 --- a/app/models/provider/openai.rb +++ b/app/models/provider/openai.rb @@ -144,41 +144,45 @@ def langfuse_client @langfuse_client = Langfuse.new end - def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_id: nil, user_identifier: nil, prompt: nil) - return unless langfuse_client - - trace = langfuse_client.trace( - name: "openai.#{name}", - input: input, - session_id: session_id, - user_id: user_identifier - ) - generation_options = { - name: name, - model: model, - input: input, - output: output, - usage: usage, - session_id: session_id, - user_id: user_identifier - } - - if prompt.present? - generation_options[:version] = prompt[:version] if prompt[:version] - metadata = { - prompt: { - name: prompt[:name], - version: prompt[:version], - content: prompt[:content], - template: prompt[:template] - }.compact + def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_id: nil, user_identifier: nil, prompt: nil) + return unless langfuse_client + + trace = langfuse_client.trace( + name: "openai.#{name}", + input: input, + session_id: session_id, + user_id: user_identifier + ) + generation_options = { + name: name, + model: model, + input: input, + output: output, + usage: usage, + session_id: session_id, + user_id: user_identifier } - generation_options[:metadata] = metadata - end - trace.generation(**generation_options) - trace.update(output: output) - rescue => e - Rails.logger.warn("Langfuse logging failed: #{e.message}") - end + if prompt.present? + generation_options[:prompt_name] = prompt[:name] if prompt[:name] + generation_options[:prompt_version] = prompt[:version] if prompt[:version] + generation_options[:prompt_id] = prompt[:id] if prompt[:id] + + metadata = { + prompt: { + id: prompt[:id], + name: prompt[:name], + version: prompt[:version], + content: prompt[:content], + template: prompt[:template] + }.compact + } + generation_options[:metadata] = metadata + end + + trace.generation(**generation_options) + trace.update(output: output) + rescue => e + Rails.logger.warn("Langfuse logging failed: #{e.message}") + end end From 805d4051a59813cdf8238c3dfef16acbdc8b0178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Wed, 1 Oct 2025 20:30:22 +0000 Subject: [PATCH 4/4] Fix {{variable}} replacement --- app/models/assistant/configurable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/assistant/configurable.rb b/app/models/assistant/configurable.rb index 1fdca9a1ce6..8c78a72d236 100644 --- a/app/models/assistant/configurable.rb +++ b/app/models/assistant/configurable.rb @@ -98,7 +98,7 @@ def interpolate_template(template, variables) when String # Replace {{ variable }} placeholders with provided variables template.gsub(/\{\{\s*(\w+)\s*\}\}/) do - key = Regexp.last_match(1) + key = Regexp.last_match(1).to_sym variables[key] || "" end when Array