mirror of
				https://github.com/discourse/discourse-ai.git
				synced 2025-10-31 14:38:37 +00:00 
			
		
		
		
	* DEV: Add icon support * DEV: Add basic setup for custom prompt menu * FEATURE: custom prompt backend * fix custom prompt param check * fix custom prompt replace * WIP * fix custom prompt usage * fixes * DEV: Update front-end * DEV: No more custom prompt state * DEV: Add specs * FIX: Title/Category/Tag suggestions Suggestion dropdowns broke because it `messages_with_user_input(user_input)` expects a hash now. * DEV: Apply syntax tree * DEV: Restrict custom prompts to configured groups * oops * fix tests * lint * I love tests * lint is cool tho --------- Co-authored-by: Rafael dos Santos Silva <xfalcox@gmail.com>
		
			
				
	
	
		
			165 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| module DiscourseAi
 | |
|   module AiHelper
 | |
|     class LlmPrompt
 | |
|       def available_prompts(name_filter: nil)
 | |
|         cp = CompletionPrompt
 | |
|         cp = cp.where(name: name_filter) if name_filter.present?
 | |
|         cp
 | |
|           .where(provider: enabled_provider)
 | |
|           .where(enabled: true)
 | |
|           .map do |prompt|
 | |
|             translation =
 | |
|               I18n.t("discourse_ai.ai_helper.prompts.#{prompt.name}", default: nil) ||
 | |
|                 prompt.translated_name || prompt.name
 | |
| 
 | |
|             {
 | |
|               id: prompt.id,
 | |
|               name: prompt.name,
 | |
|               translated_name: translation,
 | |
|               prompt_type: prompt.prompt_type,
 | |
|               icon: icon_map(prompt.name),
 | |
|             }
 | |
|           end
 | |
|       end
 | |
| 
 | |
|       def generate_and_send_prompt(prompt, params)
 | |
|         case enabled_provider
 | |
|         when "openai"
 | |
|           openai_call(prompt, params)
 | |
|         when "anthropic"
 | |
|           anthropic_call(prompt, params)
 | |
|         when "huggingface"
 | |
|           huggingface_call(prompt, params)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def enabled_provider
 | |
|         case SiteSetting.ai_helper_model
 | |
|         when /gpt/
 | |
|           "openai"
 | |
|         when /claude/
 | |
|           "anthropic"
 | |
|         else
 | |
|           "huggingface"
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       private
 | |
| 
 | |
|       def icon_map(name)
 | |
|         case name
 | |
|         when "translate"
 | |
|           "language"
 | |
|         when "generate_titles"
 | |
|           "heading"
 | |
|         when "proofread"
 | |
|           "spell-check"
 | |
|         when "markdown_table"
 | |
|           "table"
 | |
|         when "tone"
 | |
|           "microphone"
 | |
|         when "custom_prompt"
 | |
|           "comment"
 | |
|         when "rewrite"
 | |
|           "pen"
 | |
|         else
 | |
|           nil
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def generate_diff(text, suggestion)
 | |
|         cooked_text = PrettyText.cook(text)
 | |
|         cooked_suggestion = PrettyText.cook(suggestion)
 | |
| 
 | |
|         DiscourseDiff.new(cooked_text, cooked_suggestion).inline_html
 | |
|       end
 | |
| 
 | |
|       def parse_content(prompt, content)
 | |
|         return "" if content.blank?
 | |
| 
 | |
|         case enabled_provider
 | |
|         when "openai"
 | |
|           return content.strip if !prompt.list?
 | |
| 
 | |
|           content.gsub("\"", "").gsub(/\d./, "").split("\n").map(&:strip)
 | |
|         when "anthropic"
 | |
|           parse_antropic_content(prompt, content)
 | |
|         when "huggingface"
 | |
|           return [content.strip.delete_prefix('"').delete_suffix('"')] if !prompt.list?
 | |
| 
 | |
|           content.gsub("\"", "").gsub(/\d./, "").split("\n").map(&:strip)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def openai_call(prompt, params)
 | |
|         result = { type: prompt.prompt_type }
 | |
| 
 | |
|         messages = prompt.messages_with_user_input(params)
 | |
| 
 | |
|         result[:suggestions] = DiscourseAi::Inference::OpenAiCompletions
 | |
|           .perform!(messages, SiteSetting.ai_helper_model)
 | |
|           .dig(:choices)
 | |
|           .to_a
 | |
|           .flat_map { |choice| parse_content(prompt, choice.dig(:message, :content).to_s) }
 | |
|           .compact_blank
 | |
| 
 | |
|         result[:diff] = generate_diff(params[:text], result[:suggestions].first) if prompt.diff?
 | |
| 
 | |
|         result
 | |
|       end
 | |
| 
 | |
|       def anthropic_call(prompt, params)
 | |
|         result = { type: prompt.prompt_type }
 | |
| 
 | |
|         filled_message = prompt.messages_with_user_input(params)
 | |
| 
 | |
|         message =
 | |
|           filled_message.map { |msg| "#{msg["role"]}: #{msg["content"]}" }.join("\n\n") +
 | |
|             "Assistant:"
 | |
| 
 | |
|         response = DiscourseAi::Inference::AnthropicCompletions.perform!(message)
 | |
| 
 | |
|         result[:suggestions] = parse_content(prompt, response.dig(:completion))
 | |
| 
 | |
|         result[:diff] = generate_diff(params[:text], result[:suggestions].first) if prompt.diff?
 | |
| 
 | |
|         result
 | |
|       end
 | |
| 
 | |
|       def huggingface_call(prompt, params)
 | |
|         result = { type: prompt.prompt_type }
 | |
| 
 | |
|         message = prompt.messages_with_user_input(params)
 | |
| 
 | |
|         response =
 | |
|           DiscourseAi::Inference::HuggingFaceTextGeneration.perform!(
 | |
|             message,
 | |
|             SiteSetting.ai_helper_model,
 | |
|           )
 | |
| 
 | |
|         result[:suggestions] = parse_content(prompt, response.dig(:generated_text))
 | |
| 
 | |
|         result[:diff] = generate_diff(params[:text], result[:suggestions].first) if prompt.diff?
 | |
| 
 | |
|         result
 | |
|       end
 | |
| 
 | |
|       def parse_antropic_content(prompt, content)
 | |
|         if prompt.list?
 | |
|           suggestions = Nokogiri::HTML5.fragment(content).search("ai").map(&:text)
 | |
| 
 | |
|           if suggestions.length > 1
 | |
|             suggestions
 | |
|           else
 | |
|             suggestions.first.split("\n").map(&:strip)
 | |
|           end
 | |
|         else
 | |
|           [Nokogiri::HTML5.fragment(content).at("ai").text]
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |