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..8c78a72d236 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,104 @@ 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") + 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.dig(0, "content"), + preferred_currency: preferred_currency, + preferred_date_format: preferred_date_format + ) + + content = extract_prompt_content(compiled_prompt) + return if content.blank? + + { + id: prompt.respond_to?(:id) ? prompt.id : (prompt[:id] rescue nil), + 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 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 } + + # 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).to_sym + 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) + 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 @@ -78,5 +179,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..58a6ef94a3d 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,7 +144,7 @@ def langfuse_client @langfuse_client = Langfuse.new end - def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_id: nil, user_identifier: nil) + 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( @@ -150,7 +153,7 @@ def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_ session_id: session_id, user_id: user_identifier ) - trace.generation( + generation_options = { name: name, model: model, input: input, @@ -158,7 +161,26 @@ def log_langfuse_generation(name:, model:, input:, output:, usage: nil, session_ usage: usage, session_id: session_id, user_id: user_identifier - ) + } + + 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}")