mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-30 19:42:17 +00:00
Examples simulate previous interactions with an LLM and come right after the system prompt. This helps grounding the model and producing better responses.
267 lines
7.7 KiB
Ruby
267 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
module Admin
|
|
class AiPersonasController < ::Admin::AdminController
|
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
|
|
|
before_action :find_ai_persona, only: %i[edit update destroy create_user]
|
|
|
|
def index
|
|
ai_personas =
|
|
AiPersona.ordered.map do |persona|
|
|
# we use a special serializer here cause names and descriptions are
|
|
# localized for system personas
|
|
LocalizedAiPersonaSerializer.new(persona, root: false)
|
|
end
|
|
tools =
|
|
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
|
|
AiToolSerializer.new(tool, root: false)
|
|
end
|
|
AiTool
|
|
.where(enabled: true)
|
|
.each do |tool|
|
|
tools << {
|
|
id: "custom-#{tool.id}",
|
|
name:
|
|
I18n.t(
|
|
"discourse_ai.tools.custom_name",
|
|
name: tool.name.capitalize,
|
|
tool_name: tool.tool_name,
|
|
),
|
|
}
|
|
end
|
|
llms =
|
|
DiscourseAi::Configuration::LlmEnumerator.values_for_serialization(
|
|
allowed_seeded_llm_ids: SiteSetting.ai_bot_allowed_seeded_models_map,
|
|
)
|
|
render json: {
|
|
ai_personas: ai_personas,
|
|
meta: {
|
|
tools: tools,
|
|
llms: llms,
|
|
settings: {
|
|
rag_images_enabled: SiteSetting.ai_rag_images_enabled,
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
def new
|
|
end
|
|
|
|
def edit
|
|
render json: LocalizedAiPersonaSerializer.new(@ai_persona)
|
|
end
|
|
|
|
def create
|
|
ai_persona = AiPersona.new(ai_persona_params.except(:rag_uploads))
|
|
if ai_persona.save
|
|
RagDocumentFragment.link_target_and_uploads(ai_persona, attached_upload_ids)
|
|
|
|
render json: {
|
|
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
|
|
},
|
|
status: :created
|
|
else
|
|
render_json_error ai_persona
|
|
end
|
|
end
|
|
|
|
def create_user
|
|
user = @ai_persona.create_user!
|
|
render json: BasicUserSerializer.new(user, root: "user")
|
|
end
|
|
|
|
def update
|
|
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
|
|
RagDocumentFragment.update_target_uploads(@ai_persona, attached_upload_ids)
|
|
|
|
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
|
else
|
|
render_json_error @ai_persona
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
if @ai_persona.destroy
|
|
head :no_content
|
|
else
|
|
render_json_error @ai_persona
|
|
end
|
|
end
|
|
|
|
def stream_reply
|
|
persona =
|
|
AiPersona.find_by(name: params[:persona_name]) ||
|
|
AiPersona.find_by(id: params[:persona_id])
|
|
return render_json_error(I18n.t("discourse_ai.errors.persona_not_found")) if persona.nil?
|
|
|
|
return render_json_error(I18n.t("discourse_ai.errors.persona_disabled")) if !persona.enabled
|
|
|
|
if persona.default_llm.blank?
|
|
return render_json_error(I18n.t("discourse_ai.errors.no_default_llm"))
|
|
end
|
|
|
|
if params[:query].blank?
|
|
return render_json_error(I18n.t("discourse_ai.errors.no_query_specified"))
|
|
end
|
|
|
|
if !persona.user_id
|
|
return render_json_error(I18n.t("discourse_ai.errors.no_user_for_persona"))
|
|
end
|
|
|
|
if !params[:username] && !params[:user_unique_id]
|
|
return render_json_error(I18n.t("discourse_ai.errors.no_user_specified"))
|
|
end
|
|
|
|
user = nil
|
|
|
|
if params[:username]
|
|
user = User.find_by_username(params[:username])
|
|
return render_json_error(I18n.t("discourse_ai.errors.user_not_found")) if user.nil?
|
|
elsif params[:user_unique_id]
|
|
user = stage_user
|
|
end
|
|
|
|
raise Discourse::NotFound if user.nil?
|
|
|
|
topic_id = params[:topic_id].to_i
|
|
topic = nil
|
|
|
|
if topic_id > 0
|
|
topic = Topic.find(topic_id)
|
|
|
|
if topic.topic_allowed_users.where(user_id: user.id).empty?
|
|
return render_json_error(I18n.t("discourse_ai.errors.user_not_allowed"))
|
|
end
|
|
end
|
|
|
|
hijack = request.env["rack.hijack"]
|
|
io = hijack.call
|
|
|
|
DiscourseAi::AiBot::ResponseHttpStreamer.queue_streamed_reply(
|
|
io: io,
|
|
persona: persona,
|
|
user: user,
|
|
topic: topic,
|
|
query: params[:query].to_s,
|
|
custom_instructions: params[:custom_instructions].to_s,
|
|
current_user: current_user,
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
AI_STREAM_CONVERSATION_UNIQUE_ID = "ai-stream-conversation-unique-id"
|
|
|
|
def stage_user
|
|
unique_id = params[:user_unique_id].to_s
|
|
field = UserCustomField.find_by(name: AI_STREAM_CONVERSATION_UNIQUE_ID, value: unique_id)
|
|
|
|
if field
|
|
field.user
|
|
else
|
|
preferred_username = params[:preferred_username]
|
|
username = UserNameSuggester.suggest(preferred_username || unique_id)
|
|
|
|
user =
|
|
User.new(
|
|
username: username,
|
|
email: "#{SecureRandom.hex}@invalid.com",
|
|
staged: true,
|
|
active: false,
|
|
)
|
|
user.custom_fields[AI_STREAM_CONVERSATION_UNIQUE_ID] = unique_id
|
|
user.save!
|
|
user
|
|
end
|
|
end
|
|
|
|
def find_ai_persona
|
|
@ai_persona = AiPersona.find(params[:id])
|
|
end
|
|
|
|
def attached_upload_ids
|
|
ai_persona_params[:rag_uploads].to_a.map { |h| h[:id] }
|
|
end
|
|
|
|
def ai_persona_params
|
|
permitted =
|
|
params.require(:ai_persona).permit(
|
|
:name,
|
|
:description,
|
|
:enabled,
|
|
:system_prompt,
|
|
:priority,
|
|
:top_p,
|
|
:temperature,
|
|
:default_llm_id,
|
|
:user_id,
|
|
:max_context_posts,
|
|
:vision_enabled,
|
|
:vision_max_pixels,
|
|
:rag_chunk_tokens,
|
|
:rag_chunk_overlap_tokens,
|
|
:rag_conversation_chunks,
|
|
:rag_llm_model_id,
|
|
:question_consolidator_llm_id,
|
|
:allow_chat_channel_mentions,
|
|
:allow_chat_direct_messages,
|
|
:allow_topic_mentions,
|
|
:allow_personal_messages,
|
|
:tool_details,
|
|
:forced_tool_count,
|
|
:force_default_llm,
|
|
allowed_group_ids: [],
|
|
rag_uploads: [:id],
|
|
)
|
|
|
|
if tools = params.dig(:ai_persona, :tools)
|
|
permitted[:tools] = permit_tools(tools)
|
|
end
|
|
|
|
if response_format = params.dig(:ai_persona, :response_format)
|
|
permitted[:response_format] = permit_response_format(response_format)
|
|
end
|
|
|
|
if examples = params.dig(:ai_persona, :examples)
|
|
permitted[:examples] = permit_examples(examples)
|
|
end
|
|
|
|
permitted
|
|
end
|
|
|
|
def permit_tools(tools)
|
|
return [] if !tools.is_a?(Array)
|
|
|
|
tools.filter_map do |tool, options, force_tool|
|
|
break nil if !tool.is_a?(String)
|
|
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
|
|
|
# this is simpler from a storage perspective, 1 way to store tools
|
|
[tool, options, !!force_tool]
|
|
end
|
|
end
|
|
|
|
def permit_response_format(response_format)
|
|
return [] if !response_format.is_a?(Array)
|
|
|
|
response_format.map do |element|
|
|
if element && element.is_a?(ActionController::Parameters)
|
|
element.permit!
|
|
else
|
|
false
|
|
end
|
|
end
|
|
end
|
|
|
|
def permit_examples(examples)
|
|
return [] if !examples.is_a?(Array)
|
|
|
|
examples.map { |example_arr| example_arr.take(2).map(&:to_s) }
|
|
end
|
|
end
|
|
end
|
|
end
|