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,
|
||||
).as_json,
|
||||
meta: {
|
||||
presets: DiscourseAi::Completions::Llm.presets,
|
||||
providers: DiscourseAi::Completions::Llm.provider_names,
|
||||
tokenizers:
|
||||
DiscourseAi::Completions::Llm.tokenizer_names.map { |tn|
|
||||
|
|
|
@ -41,7 +41,7 @@ class LlmModel < ActiveRecord::Base
|
|||
new_user =
|
||||
User.new(
|
||||
id: [FIRST_BOT_USER_ID, next_id].min,
|
||||
email: "no_email_#{name.underscore}",
|
||||
email: "no_email_#{SecureRandom.hex}",
|
||||
name: name.titleize,
|
||||
username: UserNameSuggester.suggest(name),
|
||||
active: true,
|
||||
|
|
|
@ -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 { 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 BackButton from "discourse/components/back-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 AdminUser from "admin/models/admin-user";
|
||||
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 {
|
||||
@service toasts;
|
||||
@service router;
|
||||
@service dialog;
|
||||
@tracked presetConfigured = false;
|
||||
presetId = "none";
|
||||
|
||||
@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 showPresets() {
|
||||
return (
|
||||
this.args.model.isNew && !this.presetConfigured && !this.args.model.url
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
configurePreset() {
|
||||
this.presetConfigured = true;
|
||||
|
||||
let [id, model] = this.presetId.split(/-(.*)/);
|
||||
if (id === "none") {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
|
@ -150,157 +66,25 @@ export default class AiLlmEditor extends Component {
|
|||
@route="adminPlugins.show.discourse-ai-llms"
|
||||
@label="discourse_ai.llms.back"
|
||||
/>
|
||||
{{#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)}}
|
||||
{{#if this.showPresets}}
|
||||
<form class="form-horizontal ai-llm-editor">
|
||||
<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}}
|
||||
<label>{{I18n.t "discourse_ai.llms.preconfigured_llms"}}</label>
|
||||
<ComboBox
|
||||
@value={{this.presetId}}
|
||||
@content={{this.preConfiguredLlms}}
|
||||
class="ai-llm-editor__presets"
|
||||
/>
|
||||
</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">
|
||||
<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"}}
|
||||
<div class="control-group ai-llm-editor__action_panel">
|
||||
<DButton class="ai-llm-editor__next" @action={{this.configurePreset}}>
|
||||
{{I18n.t "discourse_ai.llms.next.title"}}
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<AiLlmEditorForm @model={{@model}} @llms={{@llms}} />
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export default class AiLlmsListEditor extends Component {
|
|||
{{#unless @currentLlm.isNew}}
|
||||
<LinkTo
|
||||
@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"}}
|
||||
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
|
||||
|
|
|
@ -204,6 +204,11 @@ en:
|
|||
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.
|
||||
preconfigured_llms: "Select your LLM"
|
||||
preconfigured:
|
||||
none: "Configure manually..."
|
||||
next:
|
||||
title: "Next"
|
||||
|
||||
tests:
|
||||
title: "Run Test"
|
||||
|
|
|
@ -28,7 +28,6 @@ module DiscourseAi
|
|||
end
|
||||
|
||||
def default_options(dialect)
|
||||
# skipping 2.0 support for now, since other models are better
|
||||
mapped_model =
|
||||
case model
|
||||
when "claude-2"
|
||||
|
@ -41,8 +40,10 @@ module DiscourseAi
|
|||
"claude-3-sonnet-20240229"
|
||||
when "claude-3-opus"
|
||||
"claude-3-opus-20240229"
|
||||
when "claude-3-5-sonnet"
|
||||
"claude-3-5-sonnet-20240620"
|
||||
else
|
||||
raise "Unsupported model: #{model}"
|
||||
model
|
||||
end
|
||||
|
||||
options = { model: mapped_model, max_tokens: 3_000 }
|
||||
|
|
|
@ -78,6 +78,8 @@ module DiscourseAi
|
|||
"anthropic.claude-instant-v1"
|
||||
when "claude-3-opus"
|
||||
"anthropic.claude-3-opus-20240229-v1:0"
|
||||
when "claude-3-5-sonnet"
|
||||
"anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||
else
|
||||
model
|
||||
end
|
||||
|
|
|
@ -18,6 +18,63 @@ module DiscourseAi
|
|||
UNKNOWN_MODEL = Class.new(StandardError)
|
||||
|
||||
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
|
||||
providers = %w[aws_bedrock anthropic vllm hugging_face cohere open_ai google azure]
|
||||
if !Rails.env.production?
|
||||
|
|
|
@ -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…
Reference in New Issue