Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aee2081356 | ||
|
|
e6bfec5bdb |
@@ -1,4 +1,3 @@
|
||||
< 3.3.0.beta2-dev: 61890b667c06299841ae88946f84a112f00060e1
|
||||
< 3.3.0.beta1-dev: a65c4076a63104b61d213534db7f89aa3c8acd9e
|
||||
< 3.2.0.beta5-dev: c70f43f130fb98e4a1adb1afd9b621aaa49088e3
|
||||
< 3.2.0.beta4-dev: 493b48477a9288f2f1b6f021954088705a6673d6
|
||||
|
||||
+1
-3
@@ -31,8 +31,7 @@ GEM
|
||||
racc (1.7.3)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.9.0)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
rexml (3.2.6)
|
||||
rubocop (1.61.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
@@ -61,7 +60,6 @@ GEM
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
ruby-progressbar (1.13.0)
|
||||
strscan (3.1.0)
|
||||
syntax_tree (6.2.0)
|
||||
prettier_print (>= 1.2.0)
|
||||
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")
|
||||
);
|
||||
},
|
||||
});
|
||||
-17
@@ -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
@@ -1 +0,0 @@
|
||||
<AiLlmsListEditor @llms={{this.model}} />
|
||||
-1
@@ -1 +0,0 @@
|
||||
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
|
||||
-1
@@ -1 +0,0 @@
|
||||
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
|
||||
-6
@@ -1,9 +1,3 @@
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="adminPlugins.show.discourse-ai-personas" class={{linkClass}}>
|
||||
{{@model.plugin.nameTitleized}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
|
||||
<AiPersonaListEditor
|
||||
@personas={{this.allPersonas}}
|
||||
@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_conversation_chunks,
|
||||
:question_consolidator_llm,
|
||||
:allow_chat,
|
||||
allowed_group_ids: [],
|
||||
rag_uploads: [:id],
|
||||
)
|
||||
|
||||
@@ -19,8 +19,7 @@ module DiscourseAi
|
||||
if shared_conversation.persisted?
|
||||
render json: success_json.merge(share_key: shared_conversation.share_key)
|
||||
else
|
||||
render json:
|
||||
failed_json.merge(error: I18n.t("discourse_ai.share_ai.errors.failed_to_share")),
|
||||
render json: failed_json.merge(error: I18n.t("discourse_ai.share_ai.failed_to_share")),
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -31,9 +30,7 @@ module DiscourseAi
|
||||
SharedAiConversation.destroy_conversation(@shared_conversation)
|
||||
|
||||
render json:
|
||||
success_json.merge(
|
||||
message: I18n.t("discourse_ai.share_ai.errors.conversation_deleted"),
|
||||
)
|
||||
success_json.merge(message: I18n.t("discourse_ai.share_ai.conversation_deleted"))
|
||||
end
|
||||
|
||||
def show
|
||||
|
||||
@@ -107,50 +107,20 @@ module DiscourseAi
|
||||
status: 502
|
||||
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
|
||||
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_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?
|
||||
final_image_url = get_caption_url(image, image_url)
|
||||
|
||||
hijack do
|
||||
if Rails.env.development?
|
||||
sleep 2 # Simulate a delay of 2 seconds
|
||||
caption = random_caption
|
||||
else
|
||||
caption =
|
||||
DiscourseAi::AiHelper::Assistant.new.generate_image_caption(
|
||||
final_image_url,
|
||||
current_user,
|
||||
)
|
||||
end
|
||||
caption =
|
||||
DiscourseAi::AiHelper::Assistant.new.generate_image_caption(
|
||||
final_image_url,
|
||||
current_user,
|
||||
)
|
||||
render json: {
|
||||
caption:
|
||||
"#{caption} (#{I18n.t("discourse_ai.ai_helper.image_caption.attribution")})",
|
||||
|
||||
@@ -20,7 +20,6 @@ module DiscourseAi
|
||||
term: query,
|
||||
search_context: guardian,
|
||||
use_pg_headlines_for_excerpt: false,
|
||||
can_lazy_load_categories: guardian.can_lazy_load_categories?,
|
||||
)
|
||||
|
||||
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
||||
|
||||
@@ -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
|
||||
@@ -11,7 +11,6 @@ class AiApiAuditLog < ActiveRecord::Base
|
||||
Gemini = 4
|
||||
Vllm = 5
|
||||
Cohere = 6
|
||||
Ollama = 7
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,14 +21,12 @@ end
|
||||
# id :bigint not null, primary key
|
||||
# provider_id :integer not null
|
||||
# user_id :integer
|
||||
# topic_id :integer
|
||||
# post_id :integer
|
||||
# request_tokens :integer
|
||||
# response_tokens :integer
|
||||
# raw_request_payload :string
|
||||
# raw_response_payload :string
|
||||
# created_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
@@ -8,7 +8,6 @@ class AiPersona < ActiveRecord::Base
|
||||
validates :description, presence: true, length: { maximum: 2000 }
|
||||
validates :system_prompt, presence: true, length: { maximum: 10_000_000 }
|
||||
validate :system_persona_unchangeable, on: :update, if: :system
|
||||
validate :chat_preconditions
|
||||
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
||||
# leaves some room for growth but sets a maximum to avoid memory issues
|
||||
# we may want to revisit this in the future
|
||||
@@ -53,9 +52,9 @@ class AiPersona < ActiveRecord::Base
|
||||
.where(enabled: true)
|
||||
.joins(:user)
|
||||
.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,
|
||||
user_id: user_id,
|
||||
@@ -63,7 +62,6 @@ class AiPersona < ActiveRecord::Base
|
||||
allowed_group_ids: allowed_group_ids,
|
||||
default_llm: default_llm,
|
||||
mentionable: mentionable,
|
||||
allow_chat: allow_chat,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -74,20 +72,23 @@ class AiPersona < ActiveRecord::Base
|
||||
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)
|
||||
all_mentionables =
|
||||
persona_cache[:mentionables] ||= persona_users.select do |mentionable|
|
||||
mentionable[:mentionable]
|
||||
end
|
||||
persona_cache[:mentionable_usernames] ||= AiPersona
|
||||
.where(mentionable: true)
|
||||
.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
|
||||
all_mentionables.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
|
||||
else
|
||||
@@ -113,7 +114,6 @@ class AiPersona < ActiveRecord::Base
|
||||
vision_max_pixels = self.vision_max_pixels
|
||||
rag_conversation_chunks = self.rag_conversation_chunks
|
||||
question_consolidator_llm = self.question_consolidator_llm
|
||||
allow_chat = self.allow_chat
|
||||
|
||||
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
||||
if persona_class
|
||||
@@ -133,10 +133,6 @@ class AiPersona < ActiveRecord::Base
|
||||
user_id
|
||||
end
|
||||
|
||||
persona_class.define_singleton_method :allow_chat do
|
||||
allow_chat
|
||||
end
|
||||
|
||||
persona_class.define_singleton_method :mentionable do
|
||||
mentionable
|
||||
end
|
||||
@@ -256,10 +252,6 @@ class AiPersona < ActiveRecord::Base
|
||||
question_consolidator_llm
|
||||
end
|
||||
|
||||
define_singleton_method :allow_chat do
|
||||
allow_chat
|
||||
end
|
||||
|
||||
define_singleton_method :to_s do
|
||||
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
|
||||
end
|
||||
@@ -350,12 +342,6 @@ class AiPersona < ActiveRecord::Base
|
||||
|
||||
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
|
||||
if top_p_changed? || temperature_changed? || system_prompt_changed? || commands_changed? ||
|
||||
name_changed? || description_changed?
|
||||
@@ -400,14 +386,7 @@ end
|
||||
# rag_chunk_tokens :integer default(374), not null
|
||||
# rag_chunk_overlap_tokens :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
|
||||
# allow_chat :boolean default(FALSE), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -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
|
||||
#
|
||||
@@ -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
|
||||
#
|
||||
@@ -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_overlap_tokens,
|
||||
:rag_conversation_chunks,
|
||||
:question_consolidator_llm,
|
||||
:allow_chat
|
||||
:question_consolidator_llm
|
||||
|
||||
has_one :user, serializer: BasicUserSerializer, embed: :object
|
||||
has_many :rag_uploads, serializer: UploadSerializer, embed: :object
|
||||
|
||||
@@ -8,10 +8,5 @@ export default {
|
||||
this.route("new");
|
||||
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_conversation_chunks",
|
||||
"question_consolidator_llm",
|
||||
"allow_chat",
|
||||
];
|
||||
|
||||
const SYSTEM_ATTRIBUTES = [
|
||||
@@ -47,7 +46,6 @@ const SYSTEM_ATTRIBUTES = [
|
||||
"rag_chunk_overlap_tokens",
|
||||
"rag_conversation_chunks",
|
||||
"question_consolidator_llm",
|
||||
"allow_chat",
|
||||
];
|
||||
|
||||
class CommandOption {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { service } from "@ember/service";
|
||||
import { gt } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
||||
import AiBotHeaderPanel from "./ai-bot-header-panel";
|
||||
|
||||
export default class AiBotHeaderIcon extends Component {
|
||||
@service siteSettings;
|
||||
@@ -24,12 +26,24 @@ export default class AiBotHeaderIcon extends Component {
|
||||
<template>
|
||||
{{#if (gt this.bots.length 0)}}
|
||||
<li>
|
||||
<DButton
|
||||
@action={{this.compose}}
|
||||
@icon="robot"
|
||||
title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
|
||||
class="ai-bot-button icon btn-flat"
|
||||
/>
|
||||
{{#if (gt this.bots.length 1)}}
|
||||
<DMenu
|
||||
@icon="robot"
|
||||
@title={{i18n "discourse_ai.ai_bot.shortcut_title"}}
|
||||
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>
|
||||
{{/if}}
|
||||
</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
Reference in New Issue
Block a user