Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea1e8efd8 | ||
|
|
2b98e84acf | ||
|
|
a1c649965f | ||
|
|
baf88e7cfc | ||
|
|
d8a0f44fed | ||
|
|
f8381c0e8a | ||
|
|
d5c23f01ff | ||
|
|
06137ac706 | ||
|
|
59e63a2da9 | ||
|
|
3a9080dd14 | ||
|
|
2b473dd4a5 | ||
|
|
d4116ecfac | ||
|
|
232f12eba6 | ||
|
|
4d8d822351 | ||
|
|
d8ebed8fb5 | ||
|
|
1d786fbaaf | ||
|
|
255139056d | ||
|
|
d64cff7692 | ||
|
|
1e6d9ed634 | ||
|
|
3db89bfdc8 | ||
|
|
28647b81fe | ||
|
|
d5e30592f3 | ||
|
|
fc73cce113 | ||
|
|
cb23ae614f | ||
|
|
8eee6893d6 | ||
|
|
8b00c47087 | ||
|
|
e22194f321 | ||
|
|
62fc7d6ed0 | ||
|
|
5c02b885ea | ||
|
|
5861418e9d | ||
|
|
0069256efd | ||
|
|
64f41454ad | ||
|
|
61890b667c | ||
|
|
514823daca | ||
|
|
cf34838a09 | ||
|
|
ab4544d897 | ||
|
|
27827c0898 | ||
|
|
4f1a3effe0 | ||
|
|
dacc1b9f28 | ||
|
|
ab78d9b597 | ||
|
|
88c7427fab | ||
|
|
37a2db5223 | ||
|
|
e4b326c711 | ||
|
|
8875830f6a |
@@ -1,3 +1,4 @@
|
|||||||
|
< 3.3.0.beta2-dev: 61890b667c06299841ae88946f84a112f00060e1
|
||||||
< 3.3.0.beta1-dev: a65c4076a63104b61d213534db7f89aa3c8acd9e
|
< 3.3.0.beta1-dev: a65c4076a63104b61d213534db7f89aa3c8acd9e
|
||||||
< 3.2.0.beta5-dev: c70f43f130fb98e4a1adb1afd9b621aaa49088e3
|
< 3.2.0.beta5-dev: c70f43f130fb98e4a1adb1afd9b621aaa49088e3
|
||||||
< 3.2.0.beta4-dev: 493b48477a9288f2f1b6f021954088705a6673d6
|
< 3.2.0.beta4-dev: 493b48477a9288f2f1b6f021954088705a6673d6
|
||||||
|
|||||||
+3
-1
@@ -31,7 +31,8 @@ GEM
|
|||||||
racc (1.7.3)
|
racc (1.7.3)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.9.0)
|
regexp_parser (2.9.0)
|
||||||
rexml (3.2.6)
|
rexml (3.2.8)
|
||||||
|
strscan (>= 3.0.9)
|
||||||
rubocop (1.61.0)
|
rubocop (1.61.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
@@ -60,6 +61,7 @@ GEM
|
|||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
|
strscan (3.1.0)
|
||||||
syntax_tree (6.2.0)
|
syntax_tree (6.2.0)
|
||||||
prettier_print (>= 1.2.0)
|
prettier_print (>= 1.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default DiscourseRoute.extend({
|
||||||
|
async model() {
|
||||||
|
const record = this.store.createRecord("ai-llm");
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
this._super(controller, model);
|
||||||
|
controller.set(
|
||||||
|
"allLlms",
|
||||||
|
this.modelFor("adminPlugins.show.discourse-ai-llms")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default DiscourseRoute.extend({
|
||||||
|
async model(params) {
|
||||||
|
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
|
||||||
|
const id = parseInt(params.id, 10);
|
||||||
|
return allLlms.findBy("id", id);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
this._super(controller, model);
|
||||||
|
controller.set(
|
||||||
|
"allLlms",
|
||||||
|
this.modelFor("adminPlugins.show.discourse-ai-llms")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class DiscourseAiAiLlmsRoute extends DiscourseRoute {
|
||||||
|
model() {
|
||||||
|
return this.store.findAll("ai-llm");
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<AiLlmsListEditor @llms={{this.model}} />
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
|
||||||
+6
@@ -1,3 +1,9 @@
|
|||||||
|
<DBreadcrumbsItem as |linkClass|>
|
||||||
|
<LinkTo @route="adminPlugins.show.discourse-ai-personas" class={{linkClass}}>
|
||||||
|
{{@model.plugin.nameTitleized}}
|
||||||
|
</LinkTo>
|
||||||
|
</DBreadcrumbsItem>
|
||||||
|
|
||||||
<AiPersonaListEditor
|
<AiPersonaListEditor
|
||||||
@personas={{this.allPersonas}}
|
@personas={{this.allPersonas}}
|
||||||
@currentPersona={{this.model}}
|
@currentPersona={{this.model}}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Admin
|
||||||
|
class AiLlmsController < ::Admin::AdminController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
|
def index
|
||||||
|
llms = LlmModel.all
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
ai_llms:
|
||||||
|
ActiveModel::ArraySerializer.new(
|
||||||
|
llms,
|
||||||
|
each_serializer: LlmModelSerializer,
|
||||||
|
root: false,
|
||||||
|
).as_json,
|
||||||
|
meta: {
|
||||||
|
providers: DiscourseAi::Completions::Llm.provider_names,
|
||||||
|
tokenizers:
|
||||||
|
DiscourseAi::Completions::Llm.tokenizer_names.map { |tn|
|
||||||
|
{ id: tn, name: tn.split("::").last }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
llm_model = LlmModel.find(params[:id])
|
||||||
|
render json: LlmModelSerializer.new(llm_model)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
llm_model = LlmModel.new(ai_llm_params)
|
||||||
|
if llm_model.save
|
||||||
|
render json: { ai_persona: llm_model }, status: :created
|
||||||
|
else
|
||||||
|
render_json_error llm_model
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
llm_model = LlmModel.find(params[:id])
|
||||||
|
|
||||||
|
if llm_model.update(ai_llm_params)
|
||||||
|
render json: llm_model
|
||||||
|
else
|
||||||
|
render_json_error llm_model
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test
|
||||||
|
RateLimiter.new(current_user, "llm_test_#{current_user.id}", 3, 1.minute).performed!
|
||||||
|
|
||||||
|
llm_model = LlmModel.new(ai_llm_params)
|
||||||
|
|
||||||
|
DiscourseAi::Completions::Llm.proxy_from_obj(llm_model).generate(
|
||||||
|
"How much is 1 + 1?",
|
||||||
|
user: current_user,
|
||||||
|
feature_name: "llm_validator",
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: { success: true }
|
||||||
|
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed => e
|
||||||
|
render json: { success: false, error: e.message }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ai_llm_params
|
||||||
|
params.require(:ai_llm).permit(
|
||||||
|
:display_name,
|
||||||
|
:name,
|
||||||
|
:provider,
|
||||||
|
:tokenizer,
|
||||||
|
:max_prompt_tokens,
|
||||||
|
:url,
|
||||||
|
:api_key,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -125,6 +125,7 @@ module DiscourseAi
|
|||||||
:rag_chunk_overlap_tokens,
|
:rag_chunk_overlap_tokens,
|
||||||
:rag_conversation_chunks,
|
:rag_conversation_chunks,
|
||||||
:question_consolidator_llm,
|
:question_consolidator_llm,
|
||||||
|
:allow_chat,
|
||||||
allowed_group_ids: [],
|
allowed_group_ids: [],
|
||||||
rag_uploads: [:id],
|
rag_uploads: [:id],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ module DiscourseAi
|
|||||||
if shared_conversation.persisted?
|
if shared_conversation.persisted?
|
||||||
render json: success_json.merge(share_key: shared_conversation.share_key)
|
render json: success_json.merge(share_key: shared_conversation.share_key)
|
||||||
else
|
else
|
||||||
render json: failed_json.merge(error: I18n.t("discourse_ai.share_ai.failed_to_share")),
|
render json:
|
||||||
|
failed_json.merge(error: I18n.t("discourse_ai.share_ai.errors.failed_to_share")),
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -30,7 +31,9 @@ module DiscourseAi
|
|||||||
SharedAiConversation.destroy_conversation(@shared_conversation)
|
SharedAiConversation.destroy_conversation(@shared_conversation)
|
||||||
|
|
||||||
render json:
|
render json:
|
||||||
success_json.merge(message: I18n.t("discourse_ai.share_ai.conversation_deleted"))
|
success_json.merge(
|
||||||
|
message: I18n.t("discourse_ai.share_ai.errors.conversation_deleted"),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|||||||
@@ -107,20 +107,50 @@ module DiscourseAi
|
|||||||
status: 502
|
status: 502
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def random_caption
|
||||||
|
captions = [
|
||||||
|
"A beautiful landscape",
|
||||||
|
"An adorable puppy",
|
||||||
|
"A delicious meal",
|
||||||
|
"A cozy fireplace",
|
||||||
|
"A stunning sunset",
|
||||||
|
"A charming cityscape",
|
||||||
|
"A peaceful garden",
|
||||||
|
"A majestic mountain range",
|
||||||
|
"A captivating work of art",
|
||||||
|
]
|
||||||
|
captions.sample
|
||||||
|
end
|
||||||
|
|
||||||
def caption_image
|
def caption_image
|
||||||
image_url = params[:image_url]
|
image_url = params[:image_url]
|
||||||
|
image_url_type = params[:image_url_type]
|
||||||
|
|
||||||
raise Discourse::InvalidParameters.new(:image_url) if !image_url
|
raise Discourse::InvalidParameters.new(:image_url) if !image_url
|
||||||
|
raise Discourse::InvalidParameters.new(:image_url) if !image_url_type
|
||||||
|
|
||||||
|
if image_url_type == "short_path"
|
||||||
|
image = Upload.find_by(sha1: Upload.sha1_from_short_path(image_url))
|
||||||
|
elsif image_url_type == "short_url"
|
||||||
|
image = Upload.find_by(sha1: Upload.sha1_from_short_url(image_url))
|
||||||
|
else
|
||||||
|
image = upload_from_full_url(image_url)
|
||||||
|
end
|
||||||
|
|
||||||
image = upload_from_full_url(image_url)
|
|
||||||
raise Discourse::NotFound if image.blank?
|
raise Discourse::NotFound if image.blank?
|
||||||
final_image_url = get_caption_url(image, image_url)
|
final_image_url = get_caption_url(image, image_url)
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
caption =
|
if Rails.env.development?
|
||||||
DiscourseAi::AiHelper::Assistant.new.generate_image_caption(
|
sleep 2 # Simulate a delay of 2 seconds
|
||||||
final_image_url,
|
caption = random_caption
|
||||||
current_user,
|
else
|
||||||
)
|
caption =
|
||||||
|
DiscourseAi::AiHelper::Assistant.new.generate_image_caption(
|
||||||
|
final_image_url,
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
end
|
||||||
render json: {
|
render json: {
|
||||||
caption:
|
caption:
|
||||||
"#{caption} (#{I18n.t("discourse_ai.ai_helper.image_caption.attribution")})",
|
"#{caption} (#{I18n.t("discourse_ai.ai_helper.image_caption.attribution")})",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ module DiscourseAi
|
|||||||
term: query,
|
term: query,
|
||||||
search_context: guardian,
|
search_context: guardian,
|
||||||
use_pg_headlines_for_excerpt: false,
|
use_pg_headlines_for_excerpt: false,
|
||||||
|
can_lazy_load_categories: guardian.can_lazy_load_categories?,
|
||||||
)
|
)
|
||||||
|
|
||||||
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ::Jobs
|
||||||
|
class CreateAiChatReply < ::Jobs::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
channel = ::Chat::Channel.find_by(id: args[:channel_id])
|
||||||
|
return if channel.blank?
|
||||||
|
|
||||||
|
message = ::Chat::Message.find_by(id: args[:message_id])
|
||||||
|
return if message.blank?
|
||||||
|
|
||||||
|
personaClass =
|
||||||
|
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
||||||
|
return if personaClass.blank?
|
||||||
|
|
||||||
|
user = User.find_by(id: personaClass.user_id)
|
||||||
|
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
|
||||||
|
|
||||||
|
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
|
||||||
|
message,
|
||||||
|
channel,
|
||||||
|
args[:context_post_ids],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,6 +11,7 @@ class AiApiAuditLog < ActiveRecord::Base
|
|||||||
Gemini = 4
|
Gemini = 4
|
||||||
Vllm = 5
|
Vllm = 5
|
||||||
Cohere = 6
|
Cohere = 6
|
||||||
|
Ollama = 7
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,12 +22,14 @@ end
|
|||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# provider_id :integer not null
|
# provider_id :integer not null
|
||||||
# user_id :integer
|
# user_id :integer
|
||||||
# topic_id :integer
|
|
||||||
# post_id :integer
|
|
||||||
# request_tokens :integer
|
# request_tokens :integer
|
||||||
# response_tokens :integer
|
# response_tokens :integer
|
||||||
# raw_request_payload :string
|
# raw_request_payload :string
|
||||||
# raw_response_payload :string
|
# raw_response_payload :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# topic_id :integer
|
||||||
|
# post_id :integer
|
||||||
|
# feature_name :string(255)
|
||||||
|
# language_model :string(255)
|
||||||
#
|
#
|
||||||
|
|||||||
+38
-17
@@ -8,6 +8,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
validates :description, presence: true, length: { maximum: 2000 }
|
validates :description, presence: true, length: { maximum: 2000 }
|
||||||
validates :system_prompt, presence: true, length: { maximum: 10_000_000 }
|
validates :system_prompt, presence: true, length: { maximum: 10_000_000 }
|
||||||
validate :system_persona_unchangeable, on: :update, if: :system
|
validate :system_persona_unchangeable, on: :update, if: :system
|
||||||
|
validate :chat_preconditions
|
||||||
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
||||||
# leaves some room for growth but sets a maximum to avoid memory issues
|
# leaves some room for growth but sets a maximum to avoid memory issues
|
||||||
# we may want to revisit this in the future
|
# we may want to revisit this in the future
|
||||||
@@ -52,9 +53,9 @@ class AiPersona < ActiveRecord::Base
|
|||||||
.where(enabled: true)
|
.where(enabled: true)
|
||||||
.joins(:user)
|
.joins(:user)
|
||||||
.pluck(
|
.pluck(
|
||||||
"ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm, mentionable",
|
"ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm, mentionable, allow_chat",
|
||||||
)
|
)
|
||||||
.map do |id, user_id, username, allowed_group_ids, default_llm, mentionable|
|
.map do |id, user_id, username, allowed_group_ids, default_llm, mentionable, allow_chat|
|
||||||
{
|
{
|
||||||
id: id,
|
id: id,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
@@ -62,6 +63,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
allowed_group_ids: allowed_group_ids,
|
allowed_group_ids: allowed_group_ids,
|
||||||
default_llm: default_llm,
|
default_llm: default_llm,
|
||||||
mentionable: mentionable,
|
mentionable: mentionable,
|
||||||
|
allow_chat: allow_chat,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,23 +74,20 @@ class AiPersona < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.allowed_chat(user: nil)
|
||||||
|
personas = persona_cache[:allowed_chat] ||= persona_users.select { |u| u[:allow_chat] }
|
||||||
|
if user
|
||||||
|
personas.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
|
||||||
|
else
|
||||||
|
personas
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.mentionables(user: nil)
|
def self.mentionables(user: nil)
|
||||||
all_mentionables =
|
all_mentionables =
|
||||||
persona_cache[:mentionable_usernames] ||= AiPersona
|
persona_cache[:mentionables] ||= persona_users.select do |mentionable|
|
||||||
.where(mentionable: true)
|
mentionable[:mentionable]
|
||||||
.where(enabled: true)
|
end
|
||||||
.joins(:user)
|
|
||||||
.pluck("ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm")
|
|
||||||
.map do |id, user_id, username, allowed_group_ids, default_llm|
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
user_id: user_id,
|
|
||||||
username: username,
|
|
||||||
allowed_group_ids: allowed_group_ids,
|
|
||||||
default_llm: default_llm,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
if user
|
if user
|
||||||
all_mentionables.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
|
all_mentionables.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
|
||||||
else
|
else
|
||||||
@@ -114,6 +113,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
vision_max_pixels = self.vision_max_pixels
|
vision_max_pixels = self.vision_max_pixels
|
||||||
rag_conversation_chunks = self.rag_conversation_chunks
|
rag_conversation_chunks = self.rag_conversation_chunks
|
||||||
question_consolidator_llm = self.question_consolidator_llm
|
question_consolidator_llm = self.question_consolidator_llm
|
||||||
|
allow_chat = self.allow_chat
|
||||||
|
|
||||||
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
||||||
if persona_class
|
if persona_class
|
||||||
@@ -133,6 +133,10 @@ class AiPersona < ActiveRecord::Base
|
|||||||
user_id
|
user_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
persona_class.define_singleton_method :allow_chat do
|
||||||
|
allow_chat
|
||||||
|
end
|
||||||
|
|
||||||
persona_class.define_singleton_method :mentionable do
|
persona_class.define_singleton_method :mentionable do
|
||||||
mentionable
|
mentionable
|
||||||
end
|
end
|
||||||
@@ -252,6 +256,10 @@ class AiPersona < ActiveRecord::Base
|
|||||||
question_consolidator_llm
|
question_consolidator_llm
|
||||||
end
|
end
|
||||||
|
|
||||||
|
define_singleton_method :allow_chat do
|
||||||
|
allow_chat
|
||||||
|
end
|
||||||
|
|
||||||
define_singleton_method :to_s do
|
define_singleton_method :to_s do
|
||||||
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
|
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
|
||||||
end
|
end
|
||||||
@@ -342,6 +350,12 @@ class AiPersona < ActiveRecord::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def chat_preconditions
|
||||||
|
if allow_chat && !default_llm
|
||||||
|
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.personas.default_llm_required"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def system_persona_unchangeable
|
def system_persona_unchangeable
|
||||||
if top_p_changed? || temperature_changed? || system_prompt_changed? || commands_changed? ||
|
if top_p_changed? || temperature_changed? || system_prompt_changed? || commands_changed? ||
|
||||||
name_changed? || description_changed?
|
name_changed? || description_changed?
|
||||||
@@ -386,7 +400,14 @@ end
|
|||||||
# rag_chunk_tokens :integer default(374), not null
|
# rag_chunk_tokens :integer default(374), not null
|
||||||
# rag_chunk_overlap_tokens :integer default(10), not null
|
# rag_chunk_overlap_tokens :integer default(10), not null
|
||||||
# rag_conversation_chunks :integer default(10), not null
|
# rag_conversation_chunks :integer default(10), not null
|
||||||
|
# role :enum default("bot"), not null
|
||||||
|
# role_category_ids :integer default([]), not null, is an Array
|
||||||
|
# role_tags :string default([]), not null, is an Array
|
||||||
|
# role_group_ids :integer default([]), not null, is an Array
|
||||||
|
# role_whispers :boolean default(FALSE), not null
|
||||||
|
# role_max_responses_per_hour :integer default(50), not null
|
||||||
# question_consolidator_llm :text
|
# question_consolidator_llm :text
|
||||||
|
# allow_chat :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ChatMessageCustomPrompt < ActiveRecord::Base
|
||||||
|
# belongs_to chat message but going to avoid the cross dependency for now
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: message_custom_prompts
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# message_id :bigint not null
|
||||||
|
# custom_prompt :json not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_message_custom_prompts_on_message_id (message_id) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class LlmModel < ActiveRecord::Base
|
||||||
|
def tokenizer_class
|
||||||
|
tokenizer.constantize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: llm_models
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# display_name :string
|
||||||
|
# name :string not null
|
||||||
|
# provider :string not null
|
||||||
|
# tokenizer :string not null
|
||||||
|
# max_prompt_tokens :integer not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# url :string
|
||||||
|
# api_key :string
|
||||||
|
#
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class LlmModelSerializer < ApplicationSerializer
|
||||||
|
root "llm"
|
||||||
|
|
||||||
|
attributes :id, :display_name, :name, :provider, :max_prompt_tokens, :tokenizer, :api_key, :url
|
||||||
|
end
|
||||||
@@ -23,7 +23,8 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer
|
|||||||
:rag_chunk_tokens,
|
:rag_chunk_tokens,
|
||||||
:rag_chunk_overlap_tokens,
|
:rag_chunk_overlap_tokens,
|
||||||
:rag_conversation_chunks,
|
:rag_conversation_chunks,
|
||||||
:question_consolidator_llm
|
:question_consolidator_llm,
|
||||||
|
:allow_chat
|
||||||
|
|
||||||
has_one :user, serializer: BasicUserSerializer, embed: :object
|
has_one :user, serializer: BasicUserSerializer, embed: :object
|
||||||
has_many :rag_uploads, serializer: UploadSerializer, embed: :object
|
has_many :rag_uploads, serializer: UploadSerializer, embed: :object
|
||||||
|
|||||||
@@ -8,5 +8,10 @@ export default {
|
|||||||
this.route("new");
|
this.route("new");
|
||||||
this.route("show", { path: "/:id" });
|
this.route("show", { path: "/:id" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route("discourse-ai-llms", { path: "ai-llms" }, function () {
|
||||||
|
this.route("new");
|
||||||
|
this.route("show", { path: "/:id" });
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
|
export default class Adapter extends RestAdapter {
|
||||||
|
jsonMode = true;
|
||||||
|
|
||||||
|
basePath() {
|
||||||
|
return "/admin/plugins/discourse-ai/";
|
||||||
|
}
|
||||||
|
|
||||||
|
pathFor(store, type, findArgs) {
|
||||||
|
// removes underscores which are implemented in base
|
||||||
|
let path =
|
||||||
|
this.basePath(store, type, findArgs) +
|
||||||
|
store.pluralize(this.apiNameFor(type));
|
||||||
|
return this.appendQueryParams(path, findArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
apiNameFor() {
|
||||||
|
return "ai-llm";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import RestModel from "discourse/models/rest";
|
||||||
|
|
||||||
|
export default class AiLlm extends RestModel {
|
||||||
|
createProperties() {
|
||||||
|
return this.getProperties(
|
||||||
|
"id",
|
||||||
|
"display_name",
|
||||||
|
"name",
|
||||||
|
"provider",
|
||||||
|
"tokenizer",
|
||||||
|
"max_prompt_tokens",
|
||||||
|
"url",
|
||||||
|
"api_key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProperties() {
|
||||||
|
const attrs = this.createProperties();
|
||||||
|
attrs.id = this.id;
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConfig() {
|
||||||
|
return await ajax(`/admin/plugins/discourse-ai/ai-llms/test.json`, {
|
||||||
|
data: { ai_llm: this.createProperties() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ const CREATE_ATTRIBUTES = [
|
|||||||
"rag_chunk_overlap_tokens",
|
"rag_chunk_overlap_tokens",
|
||||||
"rag_conversation_chunks",
|
"rag_conversation_chunks",
|
||||||
"question_consolidator_llm",
|
"question_consolidator_llm",
|
||||||
|
"allow_chat",
|
||||||
];
|
];
|
||||||
|
|
||||||
const SYSTEM_ATTRIBUTES = [
|
const SYSTEM_ATTRIBUTES = [
|
||||||
@@ -46,6 +47,7 @@ const SYSTEM_ATTRIBUTES = [
|
|||||||
"rag_chunk_overlap_tokens",
|
"rag_chunk_overlap_tokens",
|
||||||
"rag_conversation_chunks",
|
"rag_conversation_chunks",
|
||||||
"question_consolidator_llm",
|
"question_consolidator_llm",
|
||||||
|
"allow_chat",
|
||||||
];
|
];
|
||||||
|
|
||||||
class CommandOption {
|
class CommandOption {
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { service } from "@ember/service";
|
|||||||
import { gt } from "truth-helpers";
|
import { gt } from "truth-helpers";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import i18n from "discourse-common/helpers/i18n";
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
import DMenu from "float-kit/components/d-menu";
|
|
||||||
import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
||||||
import AiBotHeaderPanel from "./ai-bot-header-panel";
|
|
||||||
|
|
||||||
export default class AiBotHeaderIcon extends Component {
|
export default class AiBotHeaderIcon extends Component {
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@@ -26,24 +24,12 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
<template>
|
<template>
|
||||||
{{#if (gt this.bots.length 0)}}
|
{{#if (gt this.bots.length 0)}}
|
||||||
<li>
|
<li>
|
||||||
{{#if (gt this.bots.length 1)}}
|
<DButton
|
||||||
<DMenu
|
@action={{this.compose}}
|
||||||
@icon="robot"
|
@icon="robot"
|
||||||
@title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
|
title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
|
||||||
class="ai-bot-button icon btn-flat"
|
class="ai-bot-button icon btn-flat"
|
||||||
>
|
/>
|
||||||
<:content as |args|>
|
|
||||||
<AiBotHeaderPanel @closePanel={{args.close}} />
|
|
||||||
</:content>
|
|
||||||
</DMenu>
|
|
||||||
{{else}}
|
|
||||||
<DButton
|
|
||||||
@icon="robot"
|
|
||||||
@title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
|
|
||||||
class="ai-bot-button icon btn-flat"
|
|
||||||
@action={{this.compose}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<div class="bot-panel ai-bot-available-bot-options">
|
|
||||||
{{#each this.botNames as |bot|}}
|
|
||||||
<DButton
|
|
||||||
@translatedTitle={{bot.humanized}}
|
|
||||||
@translatedLabel={{bot.humanized}}
|
|
||||||
@action={{action "composeMessageWithTargetBot" bot.modelName}}
|
|
||||||
class="btn-flat ai-bot-available-bot-content"
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import I18n from "I18n";
|
|
||||||
import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
|
|
||||||
|
|
||||||
export default class AiBotHeaderPanel extends Component {
|
|
||||||
@service siteSettings;
|
|
||||||
@service composer;
|
|
||||||
|
|
||||||
@action
|
|
||||||
composeMessageWithTargetBot(target) {
|
|
||||||
this.#composeAiBotMessage(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
get botNames() {
|
|
||||||
return this.enabledBotOptions.map((bot) => {
|
|
||||||
return {
|
|
||||||
humanized: I18n.t(`discourse_ai.ai_bot.bot_names.${bot}`),
|
|
||||||
modelName: bot,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get enabledBotOptions() {
|
|
||||||
return this.siteSettings.ai_bot_enabled_chat_bots.split("|");
|
|
||||||
}
|
|
||||||
|
|
||||||
#composeAiBotMessage(targetBot) {
|
|
||||||
this.args.closePanel();
|
|
||||||
composeAiBotMessage(targetBot, this.composer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { Input } from "@ember/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { later } from "@ember/runloop";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import BackButton from "discourse/components/back-button";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
|
import DTooltip from "float-kit/components/d-tooltip";
|
||||||
|
|
||||||
|
export default class AiLlmEditor extends Component {
|
||||||
|
@service toasts;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
@tracked isSaving = false;
|
||||||
|
|
||||||
|
@tracked testRunning = false;
|
||||||
|
@tracked testResult = null;
|
||||||
|
@tracked testError = null;
|
||||||
|
|
||||||
|
get selectedProviders() {
|
||||||
|
const t = (provName) => {
|
||||||
|
return I18n.t(`discourse_ai.llms.providers.${provName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.args.llms.resultSetMeta.providers.map((prov) => {
|
||||||
|
return { id: prov, name: t(prov) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async save() {
|
||||||
|
this.isSaving = true;
|
||||||
|
const isNew = this.args.model.isNew;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.args.model.save();
|
||||||
|
|
||||||
|
this.args.model.setProperties(result.responseJson.ai_persona);
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
this.args.llms.addObject(this.args.model);
|
||||||
|
this.router.transitionTo(
|
||||||
|
"adminPlugins.show.discourse-ai-llms.show",
|
||||||
|
this.args.model
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.toasts.success({
|
||||||
|
data: { message: I18n.t("discourse_ai.llms.saved") },
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
popupAjaxError(e);
|
||||||
|
} finally {
|
||||||
|
later(() => {
|
||||||
|
this.isSaving = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async test() {
|
||||||
|
this.testRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configTestResult = await this.args.model.testConfig();
|
||||||
|
this.testResult = configTestResult.success;
|
||||||
|
|
||||||
|
if (this.testResult) {
|
||||||
|
this.testError = null;
|
||||||
|
} else {
|
||||||
|
this.testError = configTestResult.error;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
popupAjaxError(e);
|
||||||
|
} finally {
|
||||||
|
later(() => {
|
||||||
|
this.testRunning = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get testErrorMessage() {
|
||||||
|
return I18n.t("discourse_ai.llms.tests.failure", { error: this.testError });
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayTestResult() {
|
||||||
|
return this.testRunning || this.testResult !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BackButton
|
||||||
|
@route="adminPlugins.show.discourse-ai-llms"
|
||||||
|
@label="discourse_ai.llms.back"
|
||||||
|
/>
|
||||||
|
<form class="form-horizontal ai-llm-editor">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n "discourse_ai.llms.display_name"}}</label>
|
||||||
|
<Input
|
||||||
|
class="ai-llm-editor-input ai-llm-editor__display-name"
|
||||||
|
@type="text"
|
||||||
|
@value={{@model.display_name}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n "discourse_ai.llms.name"}}</label>
|
||||||
|
<Input
|
||||||
|
class="ai-llm-editor-input ai-llm-editor__name"
|
||||||
|
@type="text"
|
||||||
|
@value={{@model.name}}
|
||||||
|
/>
|
||||||
|
<DTooltip
|
||||||
|
@icon="question-circle"
|
||||||
|
@content={{I18n.t "discourse_ai.llms.hints.name"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{I18n.t "discourse_ai.llms.provider"}}</label>
|
||||||
|
<ComboBox
|
||||||
|
@value={{@model.provider}}
|
||||||
|
@content={{this.selectedProviders}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{I18n.t "discourse_ai.llms.url"}}</label>
|
||||||
|
<Input
|
||||||
|
class="ai-llm-editor-input ai-llm-editor__url"
|
||||||
|
@type="text"
|
||||||
|
@value={{@model.url}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{I18n.t "discourse_ai.llms.api_key"}}</label>
|
||||||
|
<Input
|
||||||
|
class="ai-llm-editor-input ai-llm-editor__api-key"
|
||||||
|
@type="text"
|
||||||
|
@value={{@model.api_key}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{I18n.t "discourse_ai.llms.tokenizer"}}</label>
|
||||||
|
<ComboBox
|
||||||
|
@value={{@model.tokenizer}}
|
||||||
|
@content={{@llms.resultSetMeta.tokenizers}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n "discourse_ai.llms.max_prompt_tokens"}}</label>
|
||||||
|
<Input
|
||||||
|
@type="number"
|
||||||
|
class="ai-llm-editor-input ai-llm-editor__max-prompt-tokens"
|
||||||
|
step="any"
|
||||||
|
min="0"
|
||||||
|
lang="en"
|
||||||
|
@value={{@model.max_prompt_tokens}}
|
||||||
|
/>
|
||||||
|
<DTooltip
|
||||||
|
@icon="question-circle"
|
||||||
|
@content={{I18n.t "discourse_ai.llms.hints.max_prompt_tokens"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group ai-llm-editor__action_panel">
|
||||||
|
<DButton
|
||||||
|
class="ai-llm-editor__test"
|
||||||
|
@action={{this.test}}
|
||||||
|
@disabled={{this.testRunning}}
|
||||||
|
>
|
||||||
|
{{I18n.t "discourse_ai.llms.tests.title"}}
|
||||||
|
</DButton>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
class="btn-primary ai-llm-editor__save"
|
||||||
|
@action={{this.save}}
|
||||||
|
@disabled={{this.isSaving}}
|
||||||
|
>
|
||||||
|
{{I18n.t "discourse_ai.llms.save"}}
|
||||||
|
</DButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group ai-llm-editor-tests">
|
||||||
|
{{#if this.displayTestResult}}
|
||||||
|
{{#if this.testRunning}}
|
||||||
|
<div class="spinner small"></div>
|
||||||
|
{{I18n.t "discourse_ai.llms.tests.running"}}
|
||||||
|
{{else}}
|
||||||
|
{{#if this.testResult}}
|
||||||
|
<div class="ai-llm-editor-tests__success">
|
||||||
|
{{icon "check"}}
|
||||||
|
{{I18n.t "discourse_ai.llms.tests.success"}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="ai-llm-editor-tests__failure">
|
||||||
|
{{icon "times"}}
|
||||||
|
{{this.testErrorMessage}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { concat } from "@ember/helper";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import AiLlmEditor from "./ai-llm-editor";
|
||||||
|
|
||||||
|
export default class AiLlmsListEditor extends Component {
|
||||||
|
get hasLLMElements() {
|
||||||
|
return this.args.llms.length !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DBreadcrumbsItem as |linkClass|>
|
||||||
|
<LinkTo
|
||||||
|
@route="adminPlugins.show.discourse-ai-personas"
|
||||||
|
class={{linkClass}}
|
||||||
|
>
|
||||||
|
{{i18n "discourse_ai.llms.short_title"}}
|
||||||
|
</LinkTo>
|
||||||
|
</DBreadcrumbsItem>
|
||||||
|
|
||||||
|
<section class="ai-llms-list-editor admin-detail pull-left">
|
||||||
|
{{#if @currentLlm}}
|
||||||
|
<AiLlmEditor @model={{@currentLlm}} @llms={{@llms}} />
|
||||||
|
{{else}}
|
||||||
|
<div class="ai-llms-list-editor__header">
|
||||||
|
<h3>{{i18n "discourse_ai.llms.short_title"}}</h3>
|
||||||
|
{{#unless @currentLlm.isNew}}
|
||||||
|
<LinkTo
|
||||||
|
@route="adminPlugins.show.discourse-ai-llms.new"
|
||||||
|
class="btn btn-small btn-primary"
|
||||||
|
>
|
||||||
|
{{icon "plus"}}
|
||||||
|
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
|
||||||
|
</LinkTo>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.hasLLMElements}}
|
||||||
|
<table class="content-list ai-persona-list-editor">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
|
||||||
|
<th>{{i18n "discourse_ai.llms.provider"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each @llms as |llm|}}
|
||||||
|
<tr data-persona-id={{llm.id}} class="ai-llm-list__row">
|
||||||
|
<td><strong>{{llm.display_name}}</strong></td>
|
||||||
|
<td>{{i18n
|
||||||
|
(concat "discourse_ai.llms.providers." llm.provider)
|
||||||
|
}}</td>
|
||||||
|
<td>
|
||||||
|
<LinkTo
|
||||||
|
@route="adminPlugins.show.discourse-ai-llms.show"
|
||||||
|
current-when="true"
|
||||||
|
class="btn btn-text btn-small"
|
||||||
|
@model={{llm}}
|
||||||
|
>{{i18n "discourse_ai.llms.edit"}}</LinkTo>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user