Compare commits

..
Author SHA1 Message Date
David Taylor aee2081356 Fix lib directory 2024-05-02 19:19:09 +01:00
David Taylor e6bfec5bdb DEV: Update engine config to match skeleton
- adds `lib/` autoloading (and removes the plugin.rb workaround)
- adds eager loading of scheduled jobs in development (otherwise they don't work at all)
2024-05-02 18:53:22 +01:00
315 changed files with 97396 additions and 416282 deletions
-1
View File
@@ -1,4 +1,3 @@
< 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
+1 -3
View File
@@ -31,8 +31,7 @@ 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.8) rexml (3.2.6)
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)
@@ -61,7 +60,6 @@ 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)
@@ -1,16 +0,0 @@
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")
);
},
});
@@ -1,17 +0,0 @@
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")
);
},
});
@@ -1,7 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiAiLlmsRoute extends DiscourseRoute {
model() {
return this.store.findAll("ai-llm");
}
}
@@ -1 +0,0 @@
<AiLlmsListEditor @llms={{this.model}} />
@@ -1 +0,0 @@
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
@@ -1 +0,0 @@
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
@@ -1,9 +1,3 @@
<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}}
@@ -1,83 +0,0 @@
# 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,7 +125,6 @@ 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,8 +19,7 @@ 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: render json: failed_json.merge(error: I18n.t("discourse_ai.share_ai.failed_to_share")),
failed_json.merge(error: I18n.t("discourse_ai.share_ai.errors.failed_to_share")),
status: :unprocessable_entity status: :unprocessable_entity
end end
end end
@@ -31,9 +30,7 @@ module DiscourseAi
SharedAiConversation.destroy_conversation(@shared_conversation) SharedAiConversation.destroy_conversation(@shared_conversation)
render json: render json:
success_json.merge( success_json.merge(message: I18n.t("discourse_ai.share_ai.conversation_deleted"))
message: I18n.t("discourse_ai.share_ai.errors.conversation_deleted"),
)
end end
def show def show
@@ -107,50 +107,20 @@ 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
if Rails.env.development? caption =
sleep 2 # Simulate a delay of 2 seconds DiscourseAi::AiHelper::Assistant.new.generate_image_caption(
caption = random_caption final_image_url,
else current_user,
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,7 +20,6 @@ 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)
-28
View File
@@ -1,28 +0,0 @@
# 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
+2 -5
View File
@@ -11,7 +11,6 @@ class AiApiAuditLog < ActiveRecord::Base
Gemini = 4 Gemini = 4
Vllm = 5 Vllm = 5
Cohere = 6 Cohere = 6
Ollama = 7
end end
end end
@@ -22,14 +21,12 @@ 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)
# #
+17 -38
View File
@@ -8,7 +8,6 @@ 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
@@ -53,9 +52,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, allow_chat", "ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm, mentionable",
) )
.map do |id, user_id, username, allowed_group_ids, default_llm, mentionable, allow_chat| .map do |id, user_id, username, allowed_group_ids, default_llm, mentionable|
{ {
id: id, id: id,
user_id: user_id, user_id: user_id,
@@ -63,7 +62,6 @@ 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
@@ -74,20 +72,23 @@ 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[:mentionables] ||= persona_users.select do |mentionable| persona_cache[:mentionable_usernames] ||= AiPersona
mentionable[:mentionable] .where(mentionable: true)
end .where(enabled: true)
.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
@@ -113,7 +114,6 @@ 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,10 +133,6 @@ 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
@@ -256,10 +252,6 @@ 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
@@ -350,12 +342,6 @@ 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?
@@ -400,14 +386,7 @@ 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
# #
-20
View File
@@ -1,20 +0,0 @@
# 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
#
-23
View File
@@ -1,23 +0,0 @@
# 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
#
-7
View File
@@ -1,7 +0,0 @@
# frozen_string_literal: true
class LlmModelSerializer < ApplicationSerializer
root "llm"
attributes :id, :display_name, :name, :provider, :max_prompt_tokens, :tokenizer, :api_key, :url
end
@@ -23,8 +23,7 @@ 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,10 +8,5 @@ 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" });
});
}, },
}; };
@@ -1,21 +0,0 @@
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";
}
}
@@ -1,30 +0,0 @@
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,7 +26,6 @@ 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 = [
@@ -47,7 +46,6 @@ 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,7 +4,9 @@ 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;
@@ -24,12 +26,24 @@ export default class AiBotHeaderIcon extends Component {
<template> <template>
{{#if (gt this.bots.length 0)}} {{#if (gt this.bots.length 0)}}
<li> <li>
<DButton {{#if (gt this.bots.length 1)}}
@action={{this.compose}} <DMenu
@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>
@@ -0,0 +1,10 @@
<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>
@@ -0,0 +1,33 @@
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);
}
}
@@ -1,209 +0,0 @@
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>
}
@@ -1,74 +0,0 @@
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