FEATURE: Initial support for seeded LLMs (#756)
This commit is contained in:
parent
0687ec75c3
commit
a08d168740
|
@ -45,6 +45,10 @@ module DiscourseAi
|
|||
def update
|
||||
llm_model = LlmModel.find(params[:id])
|
||||
|
||||
if llm_model.seeded?
|
||||
return render_json_error(I18n.t("discourse_ai.llm.cannot_edit_builtin"), status: 403)
|
||||
end
|
||||
|
||||
if llm_model.update(ai_llm_params(updating: llm_model))
|
||||
llm_model.toggle_companion_user
|
||||
render json: LlmModelSerializer.new(llm_model)
|
||||
|
@ -56,6 +60,10 @@ module DiscourseAi
|
|||
def destroy
|
||||
llm_model = LlmModel.find(params[:id])
|
||||
|
||||
if llm_model.seeded?
|
||||
return render_json_error(I18n.t("discourse_ai.llm.cannot_delete_builtin"), status: 403)
|
||||
end
|
||||
|
||||
in_use_by = DiscourseAi::Configuration::LlmValidator.new.modules_using(llm_model)
|
||||
|
||||
if !in_use_by.empty?
|
||||
|
|
|
@ -89,6 +89,10 @@ class LlmModel < ActiveRecord::Base
|
|||
provider_params&.dig(key)
|
||||
end
|
||||
|
||||
def seeded?
|
||||
id < 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def required_provider_params
|
||||
|
|
|
@ -21,4 +21,16 @@ class LlmModelSerializer < ApplicationSerializer
|
|||
def used_by
|
||||
DiscourseAi::Configuration::LlmValidator.new.modules_using(object)
|
||||
end
|
||||
|
||||
def api_key
|
||||
object.seeded? ? "********" : object.api_key
|
||||
end
|
||||
|
||||
def url
|
||||
object.seeded? ? "********" : object.url
|
||||
end
|
||||
|
||||
def provider
|
||||
object.seeded? ? "CDCK" : object.provider
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,6 +62,10 @@ export default class AiLlmEditorForm extends Component {
|
|||
return this.args.model.used_by?.join(", ");
|
||||
}
|
||||
|
||||
get seeded() {
|
||||
return this.args.model.id < 0;
|
||||
}
|
||||
|
||||
get inUseWarning() {
|
||||
return I18n.t("discourse_ai.llms.in_use_warning", {
|
||||
settings: this.modulesUsingModel,
|
||||
|
@ -170,13 +174,19 @@ export default class AiLlmEditorForm extends Component {
|
|||
}
|
||||
|
||||
<template>
|
||||
{{#if this.seeded}}
|
||||
<div class="alert alert-info">
|
||||
{{icon "exclamation-circle"}}
|
||||
{{i18n "discourse_ai.llms.seeded_warning"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.modulesUsingModel}}
|
||||
<div class="alert alert-info">
|
||||
{{icon "exclamation-circle"}}
|
||||
{{this.inUseWarning}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<form class="form-horizontal ai-llm-editor">
|
||||
<form class="form-horizontal ai-llm-editor {{if this.seeded 'seeded'}}">
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.llms.display_name"}}</label>
|
||||
<Input
|
||||
|
@ -205,128 +215,133 @@ export default class AiLlmEditorForm extends Component {
|
|||
@class="ai-llm-editor__provider"
|
||||
/>
|
||||
</div>
|
||||
{{#if this.canEditURL}}
|
||||
{{#unless this.seeded}}
|
||||
{{#if this.canEditURL}}
|
||||
<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}}
|
||||
required="true"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<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}}
|
||||
required="true"
|
||||
<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"}}
|
||||
required="true"
|
||||
{{on "focusout" this.makeApiKeySecret}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.toggleApiKeySecret}}
|
||||
@icon="far-eye-slash"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{#each-in this.metaProviderParams as |field type|}}
|
||||
<div class="control-group ai-llm-editor-provider-param__{{type}}">
|
||||
<label>{{I18n.t
|
||||
(concat "discourse_ai.llms.provider_fields." field)
|
||||
}}</label>
|
||||
{{#if (eq type "checkbox")}}
|
||||
<Input
|
||||
@type={{type}}
|
||||
@checked={{mut (get @model.provider_params field)}}
|
||||
/>
|
||||
{{else}}
|
||||
<Input
|
||||
@type={{type}}
|
||||
@value={{mut (get @model.provider_params field)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each-in}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.llms.tokenizer"}}</label>
|
||||
<ComboBox
|
||||
@value={{@model.tokenizer}}
|
||||
@content={{@llms.resultSetMeta.tokenizers}}
|
||||
@class="ai-llm-editor__tokenizer"
|
||||
/>
|
||||
</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"}}
|
||||
required="true"
|
||||
{{on "focusout" this.makeApiKeySecret}}
|
||||
/>
|
||||
<DButton @action={{this.toggleApiKeySecret}} @icon="far-eye-slash" />
|
||||
</div>
|
||||
</div>
|
||||
{{#each-in this.metaProviderParams as |field type|}}
|
||||
<div class="control-group ai-llm-editor-provider-param__{{type}}">
|
||||
<label>{{I18n.t
|
||||
(concat "discourse_ai.llms.provider_fields." field)
|
||||
}}</label>
|
||||
{{#if (eq type "checkbox")}}
|
||||
<Input
|
||||
@type={{type}}
|
||||
@checked={{mut (get @model.provider_params field)}}
|
||||
/>
|
||||
{{else}}
|
||||
<Input
|
||||
@type={{type}}
|
||||
@value={{mut (get @model.provider_params field)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each-in}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.llms.tokenizer"}}</label>
|
||||
<ComboBox
|
||||
@value={{@model.tokenizer}}
|
||||
@content={{@llms.resultSetMeta.tokenizers}}
|
||||
@class="ai-llm-editor__tokenizer"
|
||||
/>
|
||||
</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}}
|
||||
required="true"
|
||||
/>
|
||||
<DTooltip
|
||||
@icon="question-circle"
|
||||
@content={{I18n.t "discourse_ai.llms.hints.max_prompt_tokens"}}
|
||||
/>
|
||||
</div>
|
||||
<div class="control-group ai-llm-editor__vision-enabled">
|
||||
<Input @type="checkbox" @checked={{@model.vision_enabled}} />
|
||||
<label>{{I18n.t "discourse_ai.llms.vision_enabled"}}</label>
|
||||
<DTooltip
|
||||
@icon="question-circle"
|
||||
@content={{I18n.t "discourse_ai.llms.hints.vision_enabled"}}
|
||||
/>
|
||||
</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>
|
||||
<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}}
|
||||
required="true"
|
||||
/>
|
||||
<DTooltip
|
||||
@icon="question-circle"
|
||||
@content={{I18n.t "discourse_ai.llms.hints.max_prompt_tokens"}}
|
||||
/>
|
||||
</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}}
|
||||
<div class="control-group ai-llm-editor__vision-enabled">
|
||||
<Input @type="checkbox" @checked={{@model.vision_enabled}} />
|
||||
<label>{{I18n.t "discourse_ai.llms.vision_enabled"}}</label>
|
||||
<DTooltip
|
||||
@icon="question-circle"
|
||||
@content={{I18n.t "discourse_ai.llms.hints.vision_enabled"}}
|
||||
/>
|
||||
</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
|
||||
@action={{this.delete}}
|
||||
class="btn-danger ai-llm-editor__delete"
|
||||
class="ai-llm-editor__test"
|
||||
@action={{this.test}}
|
||||
@disabled={{this.testRunning}}
|
||||
>
|
||||
{{I18n.t "discourse_ai.llms.delete"}}
|
||||
{{I18n.t "discourse_ai.llms.tests.title"}}
|
||||
</DButton>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{{/unless}}
|
||||
|
||||
<div class="control-group ai-llm-editor-tests">
|
||||
{{#if this.displayTestResult}}
|
||||
|
|
|
@ -236,7 +236,8 @@ en:
|
|||
back: "Back"
|
||||
confirm_delete: Are you sure you want to delete this model?
|
||||
delete: Delete
|
||||
in_use_warning:
|
||||
seeded_warning: "This model is pre-configured on your site and cannot be edited."
|
||||
in_use_warning:
|
||||
one: "This model is currently used by the %{settings} setting. If misconfigured, the feature won't work as expected."
|
||||
other: "This model is currently used by the following settings: %{settings}. If misconfigured, features won't work as expected. "
|
||||
|
||||
|
@ -268,6 +269,7 @@ en:
|
|||
google: "Google"
|
||||
azure: "Azure"
|
||||
ollama: "Ollama"
|
||||
CDCK: "CDCK"
|
||||
|
||||
provider_fields:
|
||||
access_key_id: "AWS Bedrock Access key ID"
|
||||
|
|
|
@ -312,6 +312,7 @@ en:
|
|||
disable_module_first: "You have to disable %{setting} first."
|
||||
set_llm_first: "Set %{setting} first."
|
||||
model_unreachable: "We couldn't get a response from this model. Check your settings first."
|
||||
invalid_seeded_model: "You can't use this model with this feature."
|
||||
endpoints:
|
||||
not_configured: "%{display_name} (not configured)"
|
||||
configuration_hint:
|
||||
|
@ -321,6 +322,7 @@ en:
|
|||
delete_failed:
|
||||
one: "We couldn't delete this model because %{settings} is using it. Update the setting and try again."
|
||||
other: "We couldn't delete this model because %{settings} are using it. Update the settings and try again."
|
||||
cannot_edit_builtin: "You can't edit a built-in model."
|
||||
|
||||
embeddings:
|
||||
configuration:
|
||||
|
|
|
@ -289,6 +289,16 @@ discourse_ai:
|
|||
default: "10" # 10: @trust_level_0
|
||||
allow_any: false
|
||||
refresh: true
|
||||
ai_helper_model_allowed_seeded_models:
|
||||
default: ""
|
||||
hidden: true
|
||||
type: list
|
||||
list_type: compact
|
||||
ai_helper_image_caption_model_allowed_seeded_models:
|
||||
default: ""
|
||||
hidden: true
|
||||
type: list
|
||||
list_type: compact
|
||||
|
||||
ai_embeddings_enabled:
|
||||
default: false
|
||||
|
@ -340,6 +350,11 @@ discourse_ai:
|
|||
allow_any: false
|
||||
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
||||
validator: "DiscourseAi::Configuration::LlmValidator"
|
||||
ai_embeddings_semantic_search_hyde_model_allowed_seeded_models:
|
||||
default: ""
|
||||
hidden: true
|
||||
type: list
|
||||
list_type: compact
|
||||
ai_embeddings_semantic_quick_search_enabled:
|
||||
default: false
|
||||
client: true
|
||||
|
@ -366,6 +381,11 @@ discourse_ai:
|
|||
default: ""
|
||||
hidden: true
|
||||
choices: "DiscourseAi::Configuration::LlmEnumerator.old_summarization_options + ['']"
|
||||
ai_summarization_model_allowed_seeded_models:
|
||||
default: ""
|
||||
hidden: true
|
||||
type: list
|
||||
list_type: compact
|
||||
|
||||
ai_bot_enabled:
|
||||
default: false
|
||||
|
@ -406,3 +426,8 @@ discourse_ai:
|
|||
ai_automation_max_triage_per_post_per_minute:
|
||||
default: 2
|
||||
hidden: true
|
||||
ai_automation_allowed_seeded_models:
|
||||
default: ""
|
||||
hidden: true
|
||||
type: list
|
||||
list_type: compact
|
||||
|
|
|
@ -8,7 +8,13 @@ module DiscourseAi
|
|||
FROM llm_models
|
||||
SQL
|
||||
|
||||
values.each { |value_h| value_h["id"] = "custom:#{value_h["id"]}" }
|
||||
values =
|
||||
values
|
||||
.filter do |value_h|
|
||||
value_h["id"] > 0 ||
|
||||
SiteSetting.ai_automation_allowed_seeded_models_map.includes?(value_h["id"].to_s)
|
||||
end
|
||||
.each { |value_h| value_h["id"] = "custom:#{value_h["id"]}" }
|
||||
|
||||
values
|
||||
end
|
||||
|
|
|
@ -15,6 +15,8 @@ module DiscourseAi
|
|||
return !@parent_enabled
|
||||
end
|
||||
|
||||
allowed_seeded_model?(val)
|
||||
|
||||
run_test(val).tap { |result| @unreachable = result }
|
||||
rescue StandardError => e
|
||||
raise e if Rails.env.test?
|
||||
|
@ -45,6 +47,10 @@ module DiscourseAi
|
|||
)
|
||||
end
|
||||
|
||||
if @invalid_seeded_model
|
||||
return I18n.t("discourse_ai.llm.configuration.invalid_seeded_model")
|
||||
end
|
||||
|
||||
return unless @unreachable
|
||||
|
||||
I18n.t("discourse_ai.llm.configuration.model_unreachable")
|
||||
|
@ -61,6 +67,19 @@ module DiscourseAi
|
|||
ai_summarization_enabled: :ai_summarization_model,
|
||||
}
|
||||
end
|
||||
|
||||
def allowed_seeded_model?(val)
|
||||
id = val.split(":").last
|
||||
return true if id.to_i > 0
|
||||
|
||||
setting = @opts[:name]
|
||||
allowed_list = SiteSetting.public_send("#{setting}_allowed_seeded_models")
|
||||
|
||||
if allowed_list.split("|").exclude?(id)
|
||||
@invalid_seeded_model = true
|
||||
raise Discourse::InvalidParameters.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue