mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-07-06 14:32:14 +00:00
FEATURE: LLM presets for model creation (#681)
* FEATURE: LLM presets for model creation Previous to this users needed to look up complicated settings when setting up models. This introduces and extensible preset system with Google/OpenAI/Anthropic presets. This will cover all the most common LLMs, we can always add more as we go. Additionally: - Proper support for Anthropic Claude Sonnet 3.5 - Stop blurring api keys when navigating away - this made it very complex to reuse keys
This commit is contained in:
parent
558574fa87
commit
e04a7be122
@ -16,6 +16,7 @@ module DiscourseAi
|
|||||||
root: false,
|
root: false,
|
||||||
).as_json,
|
).as_json,
|
||||||
meta: {
|
meta: {
|
||||||
|
presets: DiscourseAi::Completions::Llm.presets,
|
||||||
providers: DiscourseAi::Completions::Llm.provider_names,
|
providers: DiscourseAi::Completions::Llm.provider_names,
|
||||||
tokenizers:
|
tokenizers:
|
||||||
DiscourseAi::Completions::Llm.tokenizer_names.map { |tn|
|
DiscourseAi::Completions::Llm.tokenizer_names.map { |tn|
|
||||||
|
@ -41,7 +41,7 @@ class LlmModel < ActiveRecord::Base
|
|||||||
new_user =
|
new_user =
|
||||||
User.new(
|
User.new(
|
||||||
id: [FIRST_BOT_USER_ID, next_id].min,
|
id: [FIRST_BOT_USER_ID, next_id].min,
|
||||||
email: "no_email_#{name.underscore}",
|
email: "no_email_#{SecureRandom.hex}",
|
||||||
name: name.titleize,
|
name: name.titleize,
|
||||||
username: UserNameSuggester.suggest(name),
|
username: UserNameSuggester.suggest(name),
|
||||||
active: true,
|
active: true,
|
||||||
|
297
assets/javascripts/discourse/components/ai-llm-editor-form.gjs
Normal file
297
assets/javascripts/discourse/components/ai-llm-editor-form.gjs
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { Input } from "@ember/component";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import { later } from "@ember/runloop";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { or } from "truth-helpers";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||||
|
import Avatar from "discourse/helpers/bound-avatar-template";
|
||||||
|
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 AdminUser from "admin/models/admin-user";
|
||||||
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
|
import DTooltip from "float-kit/components/d-tooltip";
|
||||||
|
|
||||||
|
export default class AiLlmEditorForm extends Component {
|
||||||
|
@service toasts;
|
||||||
|
@service router;
|
||||||
|
@service dialog;
|
||||||
|
|
||||||
|
@tracked isSaving = false;
|
||||||
|
|
||||||
|
@tracked testRunning = false;
|
||||||
|
@tracked testResult = null;
|
||||||
|
@tracked testError = null;
|
||||||
|
@tracked apiKeySecret = true;
|
||||||
|
|
||||||
|
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) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get adminUser() {
|
||||||
|
return AdminUser.create(this.args.model?.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.index");
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
makeApiKeySecret() {
|
||||||
|
this.apiKeySecret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleApiKeySecret() {
|
||||||
|
this.apiKeySecret = !this.apiKeySecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
delete() {
|
||||||
|
return this.dialog.confirm({
|
||||||
|
message: I18n.t("discourse_ai.llms.confirm_delete"),
|
||||||
|
didConfirm: () => {
|
||||||
|
return this.args.model
|
||||||
|
.destroyRecord()
|
||||||
|
.then(() => {
|
||||||
|
this.args.llms.removeObject(this.args.model);
|
||||||
|
this.router.transitionTo(
|
||||||
|
"adminPlugins.show.discourse-ai-llms.index"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async toggleEnabledChatBot() {
|
||||||
|
this.args.model.set("enabled_chat_bot", !this.args.model.enabled_chat_bot);
|
||||||
|
if (!this.args.model.isNew) {
|
||||||
|
try {
|
||||||
|
await this.args.model.update({
|
||||||
|
enabled_chat_bot: this.args.model.enabled_chat_bot,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
popupAjaxError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#unless (or @model.url_editable @model.isNew)}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{icon "exclamation-circle"}}
|
||||||
|
{{I18n.t "discourse_ai.llms.srv_warning"}}
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
<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>
|
||||||
|
{{#if (or @model.url_editable @model.isNew)}}
|
||||||
|
<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>
|
||||||
|
{{/if}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{I18n.t "discourse_ai.llms.api_key"}}</label>
|
||||||
|
<div class="ai-llm-editor__secret-api-key-group">
|
||||||
|
<Input
|
||||||
|
@value={{@model.api_key}}
|
||||||
|
class="ai-llm-editor-input ai-llm-editor__api-key"
|
||||||
|
@type={{if this.apiKeySecret "password" "text"}}
|
||||||
|
{{on "focusout" this.makeApiKeySecret}}
|
||||||
|
/>
|
||||||
|
<DButton @action={{this.toggleApiKeySecret}} @icon="far-eye-slash" />
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<DToggleSwitch
|
||||||
|
class="ai-llm-editor__enabled-chat-bot"
|
||||||
|
@state={{@model.enabled_chat_bot}}
|
||||||
|
@label="discourse_ai.llms.enabled_chat_bot"
|
||||||
|
{{on "click" this.toggleEnabledChatBot}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{#if @model.user}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{i18n "discourse_ai.llms.ai_bot_user"}}</label>
|
||||||
|
<a
|
||||||
|
class="avatar"
|
||||||
|
href={{@model.user.path}}
|
||||||
|
data-user-card={{@model.user.username}}
|
||||||
|
>
|
||||||
|
{{Avatar @model.user.avatar_template "small"}}
|
||||||
|
</a>
|
||||||
|
<LinkTo @route="adminUser" @model={{this.adminUser}}>
|
||||||
|
{{@model.user.username}}
|
||||||
|
</LinkTo>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<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>
|
||||||
|
{{#unless @model.isNew}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.delete}}
|
||||||
|
class="btn-danger ai-llm-editor__delete"
|
||||||
|
>
|
||||||
|
{{I18n.t "discourse_ai.llms.delete"}}
|
||||||
|
</DButton>
|
||||||
|
{{/unless}}
|
||||||
|
</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,148 +1,64 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { Input } from "@ember/component";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { LinkTo } from "@ember/routing";
|
|
||||||
import { later } from "@ember/runloop";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import { or } from "truth-helpers";
|
|
||||||
import BackButton from "discourse/components/back-button";
|
import BackButton from "discourse/components/back-button";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
|
||||||
import Avatar from "discourse/helpers/bound-avatar-template";
|
|
||||||
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 I18n from "discourse-i18n";
|
||||||
import AdminUser from "admin/models/admin-user";
|
|
||||||
import ComboBox from "select-kit/components/combo-box";
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
import DTooltip from "float-kit/components/d-tooltip";
|
import AiLlmEditorForm from "./ai-llm-editor-form";
|
||||||
|
|
||||||
export default class AiLlmEditor extends Component {
|
export default class AiLlmEditor extends Component {
|
||||||
@service toasts;
|
@tracked presetConfigured = false;
|
||||||
@service router;
|
presetId = "none";
|
||||||
@service dialog;
|
|
||||||
|
|
||||||
@tracked isSaving = false;
|
get showPresets() {
|
||||||
|
return (
|
||||||
@tracked testRunning = false;
|
this.args.model.isNew && !this.presetConfigured && !this.args.model.url
|
||||||
@tracked testResult = null;
|
|
||||||
@tracked testError = null;
|
|
||||||
@tracked apiKeySecret = true;
|
|
||||||
|
|
||||||
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) };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get adminUser() {
|
|
||||||
return AdminUser.create(this.args.model?.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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.index");
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
makeApiKeySecret() {
|
|
||||||
this.apiKeySecret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
toggleApiKeySecret() {
|
|
||||||
this.apiKeySecret = !this.apiKeySecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
delete() {
|
|
||||||
return this.dialog.confirm({
|
|
||||||
message: I18n.t("discourse_ai.llms.confirm_delete"),
|
|
||||||
didConfirm: () => {
|
|
||||||
return this.args.model
|
|
||||||
.destroyRecord()
|
|
||||||
.then(() => {
|
|
||||||
this.args.llms.removeObject(this.args.model);
|
|
||||||
this.router.transitionTo(
|
|
||||||
"adminPlugins.show.discourse-ai-llms.index"
|
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
.catch(popupAjaxError);
|
|
||||||
|
get preConfiguredLlms() {
|
||||||
|
let options = [
|
||||||
|
{
|
||||||
|
id: "none",
|
||||||
|
name: I18n.t(`discourse_ai.llms.preconfigured.none`),
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.args.llms.resultSetMeta.presets.forEach((llm) => {
|
||||||
|
if (llm.models) {
|
||||||
|
llm.models.forEach((model) => {
|
||||||
|
options.push({
|
||||||
|
id: `${llm.id}-${model.name}`,
|
||||||
|
name: model.display_name,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async toggleEnabledChatBot() {
|
configurePreset() {
|
||||||
this.args.model.set("enabled_chat_bot", !this.args.model.enabled_chat_bot);
|
this.presetConfigured = true;
|
||||||
if (!this.args.model.isNew) {
|
|
||||||
try {
|
let [id, model] = this.presetId.split(/-(.*)/);
|
||||||
await this.args.model.update({
|
if (id === "none") {
|
||||||
enabled_chat_bot: this.args.model.enabled_chat_bot,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = this.args.llms.resultSetMeta.presets.findBy("id", id);
|
||||||
|
const modelInfo = info.models.findBy("name", model);
|
||||||
|
|
||||||
|
this.args.model.setProperties({
|
||||||
|
max_prompt_tokens: modelInfo.tokens,
|
||||||
|
tokenizer: info.tokenizer,
|
||||||
|
url: modelInfo.endpoint || info.endpoint,
|
||||||
|
display_name: modelInfo.display_name,
|
||||||
|
name: modelInfo.name,
|
||||||
|
provider: info.provider,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
popupAjaxError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -150,157 +66,25 @@ export default class AiLlmEditor extends Component {
|
|||||||
@route="adminPlugins.show.discourse-ai-llms"
|
@route="adminPlugins.show.discourse-ai-llms"
|
||||||
@label="discourse_ai.llms.back"
|
@label="discourse_ai.llms.back"
|
||||||
/>
|
/>
|
||||||
{{#unless (or @model.url_editable @model.isNew)}}
|
{{#if this.showPresets}}
|
||||||
<div class="alert alert-info">
|
|
||||||
{{icon "exclamation-circle"}}
|
|
||||||
{{I18n.t "discourse_ai.llms.srv_warning"}}
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
||||||
<form class="form-horizontal ai-llm-editor">
|
<form class="form-horizontal ai-llm-editor">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>{{i18n "discourse_ai.llms.display_name"}}</label>
|
<label>{{I18n.t "discourse_ai.llms.preconfigured_llms"}}</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
|
<ComboBox
|
||||||
@value={{@model.provider}}
|
@value={{this.presetId}}
|
||||||
@content={{this.selectedProviders}}
|
@content={{this.preConfiguredLlms}}
|
||||||
|
class="ai-llm-editor__presets"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{{#if (or @model.url_editable @model.isNew)}}
|
|
||||||
<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>
|
|
||||||
{{/if}}
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{I18n.t "discourse_ai.llms.api_key"}}</label>
|
|
||||||
<div class="ai-llm-editor__secret-api-key-group">
|
|
||||||
<Input
|
|
||||||
@value={{@model.api_key}}
|
|
||||||
class="ai-llm-editor-input ai-llm-editor__api-key"
|
|
||||||
@type={{if this.apiKeySecret "password" "text"}}
|
|
||||||
{{on "focusout" this.makeApiKeySecret}}
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@action={{this.toggleApiKeySecret}}
|
|
||||||
@icon="far-eye-slash"
|
|
||||||
{{on "focusout" this.makeApiKeySecret}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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">
|
|
||||||
<DToggleSwitch
|
|
||||||
class="ai-llm-editor__enabled-chat-bot"
|
|
||||||
@state={{@model.enabled_chat_bot}}
|
|
||||||
@label="discourse_ai.llms.enabled_chat_bot"
|
|
||||||
{{on "click" this.toggleEnabledChatBot}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{#if @model.user}}
|
|
||||||
<div class="control-group">
|
|
||||||
<label>{{i18n "discourse_ai.llms.ai_bot_user"}}</label>
|
|
||||||
<a
|
|
||||||
class="avatar"
|
|
||||||
href={{@model.user.path}}
|
|
||||||
data-user-card={{@model.user.username}}
|
|
||||||
>
|
|
||||||
{{Avatar @model.user.avatar_template "small"}}
|
|
||||||
</a>
|
|
||||||
<LinkTo @route="adminUser" @model={{this.adminUser}}>
|
|
||||||
{{@model.user.username}}
|
|
||||||
</LinkTo>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<div class="control-group ai-llm-editor__action_panel">
|
<div class="control-group ai-llm-editor__action_panel">
|
||||||
<DButton
|
<DButton class="ai-llm-editor__next" @action={{this.configurePreset}}>
|
||||||
class="ai-llm-editor__test"
|
{{I18n.t "discourse_ai.llms.next.title"}}
|
||||||
@action={{this.test}}
|
|
||||||
@disabled={{this.testRunning}}
|
|
||||||
>
|
|
||||||
{{I18n.t "discourse_ai.llms.tests.title"}}
|
|
||||||
</DButton>
|
</DButton>
|
||||||
|
|
||||||
<DButton
|
|
||||||
class="btn-primary ai-llm-editor__save"
|
|
||||||
@action={{this.save}}
|
|
||||||
@disabled={{this.isSaving}}
|
|
||||||
>
|
|
||||||
{{I18n.t "discourse_ai.llms.save"}}
|
|
||||||
</DButton>
|
|
||||||
{{#unless @model.isNew}}
|
|
||||||
<DButton
|
|
||||||
@action={{this.delete}}
|
|
||||||
class="btn-danger ai-llm-editor__delete"
|
|
||||||
>
|
|
||||||
{{I18n.t "discourse_ai.llms.delete"}}
|
|
||||||
</DButton>
|
|
||||||
{{/unless}}
|
|
||||||
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
|
||||||
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export default class AiLlmsListEditor extends Component {
|
|||||||
{{#unless @currentLlm.isNew}}
|
{{#unless @currentLlm.isNew}}
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="adminPlugins.show.discourse-ai-llms.new"
|
@route="adminPlugins.show.discourse-ai-llms.new"
|
||||||
class="btn btn-small btn-primary"
|
class="btn btn-small btn-primary ai-llms-list-editor__new"
|
||||||
>
|
>
|
||||||
{{icon "plus"}}
|
{{icon "plus"}}
|
||||||
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
|
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
|
||||||
|
@ -204,6 +204,11 @@ en:
|
|||||||
delete: Delete
|
delete: Delete
|
||||||
|
|
||||||
srv_warning: This LLM points to an SRV record, and its URL is not editable. You have to update the hidden "ai_vllm_endpoint_srv" setting instead.
|
srv_warning: This LLM points to an SRV record, and its URL is not editable. You have to update the hidden "ai_vllm_endpoint_srv" setting instead.
|
||||||
|
preconfigured_llms: "Select your LLM"
|
||||||
|
preconfigured:
|
||||||
|
none: "Configure manually..."
|
||||||
|
next:
|
||||||
|
title: "Next"
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
title: "Run Test"
|
title: "Run Test"
|
||||||
|
@ -28,7 +28,6 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_options(dialect)
|
def default_options(dialect)
|
||||||
# skipping 2.0 support for now, since other models are better
|
|
||||||
mapped_model =
|
mapped_model =
|
||||||
case model
|
case model
|
||||||
when "claude-2"
|
when "claude-2"
|
||||||
@ -41,8 +40,10 @@ module DiscourseAi
|
|||||||
"claude-3-sonnet-20240229"
|
"claude-3-sonnet-20240229"
|
||||||
when "claude-3-opus"
|
when "claude-3-opus"
|
||||||
"claude-3-opus-20240229"
|
"claude-3-opus-20240229"
|
||||||
|
when "claude-3-5-sonnet"
|
||||||
|
"claude-3-5-sonnet-20240620"
|
||||||
else
|
else
|
||||||
raise "Unsupported model: #{model}"
|
model
|
||||||
end
|
end
|
||||||
|
|
||||||
options = { model: mapped_model, max_tokens: 3_000 }
|
options = { model: mapped_model, max_tokens: 3_000 }
|
||||||
|
@ -78,6 +78,8 @@ module DiscourseAi
|
|||||||
"anthropic.claude-instant-v1"
|
"anthropic.claude-instant-v1"
|
||||||
when "claude-3-opus"
|
when "claude-3-opus"
|
||||||
"anthropic.claude-3-opus-20240229-v1:0"
|
"anthropic.claude-3-opus-20240229-v1:0"
|
||||||
|
when "claude-3-5-sonnet"
|
||||||
|
"anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||||
else
|
else
|
||||||
model
|
model
|
||||||
end
|
end
|
||||||
|
@ -18,6 +18,63 @@ module DiscourseAi
|
|||||||
UNKNOWN_MODEL = Class.new(StandardError)
|
UNKNOWN_MODEL = Class.new(StandardError)
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def presets
|
||||||
|
# Sam: I am not sure if it makes sense to translate model names at all
|
||||||
|
@presets ||=
|
||||||
|
begin
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "anthropic",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
name: "claude-3-5-sonnet",
|
||||||
|
tokens: 200_000,
|
||||||
|
display_name: "Claude 3.5 Sonnet",
|
||||||
|
},
|
||||||
|
{ name: "claude-3-opus", tokens: 200_000, display_name: "Claude 3 Opus" },
|
||||||
|
{ name: "claude-3-sonnet", tokens: 200_000, display_name: "Claude 3 Sonnet" },
|
||||||
|
{ name: "claude-3-haiku", tokens: 200_000, display_name: "Claude 3 Haiku" },
|
||||||
|
],
|
||||||
|
tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer,
|
||||||
|
endpoint: "https://api.anthropic.com/v1/messages",
|
||||||
|
provider: "anthropic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "google",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
name: "gemini-1.5-pro",
|
||||||
|
tokens: 800_000,
|
||||||
|
endpoint:
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest",
|
||||||
|
display_name: "Gemini 1.5 Pro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gemini-1.5-flash",
|
||||||
|
tokens: 800_000,
|
||||||
|
endpoint:
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest",
|
||||||
|
display_name: "Gemini 1.5 Flash",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||||
|
provider: "google",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "open_ai",
|
||||||
|
models: [
|
||||||
|
{ name: "gpt-4o", tokens: 131_072, display_name: "GPT-4 Omni" },
|
||||||
|
{ name: "gpt-4-turbo", tokens: 131_072, display_name: "GPT-4 Turbo" },
|
||||||
|
{ name: "gpt-3.5-turbo", tokens: 16_385, display_name: "GPT-3.5 Turbo" },
|
||||||
|
],
|
||||||
|
tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||||
|
endpoint: "https://api.openai.com/v1/chat/completions",
|
||||||
|
provider: "open_ai",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def provider_names
|
def provider_names
|
||||||
providers = %w[aws_bedrock anthropic vllm hugging_face cohere open_ai google azure]
|
providers = %w[aws_bedrock anthropic vllm hugging_face cohere open_ai google azure]
|
||||||
if !Rails.env.production?
|
if !Rails.env.production?
|
||||||
|
39
spec/system/llms/ai_llm_spec.rb
Normal file
39
spec/system/llms/ai_llm_spec.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Admin dashboard", type: :system do
|
||||||
|
fab!(:admin)
|
||||||
|
|
||||||
|
it "correctly sets defaults" do
|
||||||
|
sign_in(admin)
|
||||||
|
|
||||||
|
visit "/admin/plugins/discourse-ai/ai-llms"
|
||||||
|
|
||||||
|
find(".ai-llms-list-editor__new").click()
|
||||||
|
|
||||||
|
select_kit = PageObjects::Components::SelectKit.new(".ai-llm-editor__presets")
|
||||||
|
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value("anthropic-claude-3-haiku")
|
||||||
|
|
||||||
|
find(".ai-llm-editor__next").click()
|
||||||
|
find("input.ai-llm-editor__api-key").fill_in(with: "abcd")
|
||||||
|
|
||||||
|
find(".ai-llm-editor__save").click()
|
||||||
|
|
||||||
|
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-llms")
|
||||||
|
|
||||||
|
llm = LlmModel.order(:id).last
|
||||||
|
expect(llm.api_key).to eq("abcd")
|
||||||
|
|
||||||
|
preset = DiscourseAi::Completions::Llm.presets.find { |p| p[:id] == "anthropic" }
|
||||||
|
|
||||||
|
model_preset = preset[:models].find { |m| m[:name] == "claude-3-haiku" }
|
||||||
|
|
||||||
|
expect(llm.name).to eq("claude-3-haiku")
|
||||||
|
expect(llm.url).to eq(preset[:endpoint])
|
||||||
|
expect(llm.tokenizer).to eq(preset[:tokenizer].to_s)
|
||||||
|
expect(llm.max_prompt_tokens.to_i).to eq(model_preset[:tokens])
|
||||||
|
expect(llm.provider).to eq("anthropic")
|
||||||
|
expect(llm.display_name).to eq(model_preset[:display_name])
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user