Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8b3a4b40 | ||
|
|
da6d70da8f | ||
|
|
84b1c9af71 | ||
|
|
5cb91217bd | ||
|
|
4ebbdc043e | ||
|
|
d555f18c6f | ||
|
|
442681a3d3 | ||
|
|
eab2f74b58 | ||
|
|
38153608f8 | ||
|
|
1320eed9b2 | ||
|
|
fc081d9da6 | ||
|
|
ef4b3559cd | ||
|
|
1b0ba9197c | ||
|
|
c352cc5ba3 | ||
|
|
b671ffe7fa | ||
|
|
a5a39dd2ee | ||
|
|
a708d4dfa2 | ||
|
|
8e83c091a2 | ||
|
|
b863ddc94b | ||
|
|
af4f871096 | ||
|
|
e26c5986f2 | ||
|
|
6f5873b072 | ||
|
|
e39e0bdb4a | ||
|
|
b6f0ad157c | ||
|
|
f622e2644f | ||
|
|
1d5fa0ce6c | ||
|
|
346dd734d1 | ||
|
|
091dc626e8 | ||
|
|
3a2339902e | ||
|
|
e04a7be122 | ||
|
|
558574fa87 | ||
|
|
714caf34fe | ||
|
|
b23a3a86a6 | ||
|
|
1a8532b91b | ||
|
|
59ed6d7b3e | ||
|
|
8849caf136 | ||
|
|
ed3d5521a8 | ||
|
|
c4d9fab8e7 | ||
|
|
0d6d9a6ef5 | ||
|
|
3c45335936 | ||
|
|
8d5f901a67 | ||
|
|
cc0b222faa | ||
|
|
460f5c4553 | ||
|
|
f642a27f11 | ||
|
|
8a0c2020f2 | ||
|
|
effa6cc59f | ||
|
|
5abf80cb4e | ||
|
|
52a7dd2a4b | ||
|
|
875bb04467 | ||
|
|
212ee23698 | ||
|
|
8b81ff45b8 | ||
|
|
7a64699314 | ||
|
|
92108452f2 | ||
|
|
3993c685e1 | ||
|
|
564d2de534 | ||
|
|
97afda278b | ||
|
|
02a50a29f8 | ||
|
|
834fea672f | ||
|
|
d812ecf5da | ||
|
|
13840f68b3 | ||
|
|
bd1490a536 | ||
|
|
b487de933d | ||
|
|
dd4e305ff7 | ||
|
|
309280cbb6 | ||
|
|
dae9d6f14e | ||
|
|
71affe75bf | ||
|
|
333b331eb9 | ||
|
|
90c5e4bb0e |
+35
-26
@@ -1,7 +1,7 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
activesupport (7.1.3.2)
|
activesupport (7.1.3.4)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
@@ -13,27 +13,28 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bigdecimal (3.1.6)
|
bigdecimal (3.1.8)
|
||||||
concurrent-ruby (1.2.3)
|
concurrent-ruby (1.3.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
i18n (1.14.1)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
json (2.7.1)
|
json (2.7.2)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
minitest (5.22.2)
|
minitest (5.24.1)
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.2.0)
|
||||||
parallel (1.24.0)
|
parallel (1.25.1)
|
||||||
parser (3.3.0.5)
|
parser (3.3.3.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
prettier_print (1.2.1)
|
prettier_print (1.2.1)
|
||||||
racc (1.7.3)
|
racc (1.8.0)
|
||||||
|
rack (3.1.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.9.0)
|
regexp_parser (2.9.2)
|
||||||
rexml (3.2.8)
|
rexml (3.3.1)
|
||||||
strscan (>= 3.0.9)
|
strscan
|
||||||
rubocop (1.61.0)
|
rubocop (1.64.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
@@ -41,25 +42,33 @@ GEM
|
|||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.31.1)
|
rubocop-ast (1.31.3)
|
||||||
parser (>= 3.3.0.4)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.20.0)
|
rubocop-capybara (2.21.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-discourse (3.7.1)
|
rubocop-discourse (3.8.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
rubocop (>= 1.59.0)
|
rubocop (>= 1.59.0)
|
||||||
rubocop-capybara (>= 2.0.0)
|
rubocop-capybara (>= 2.0.0)
|
||||||
rubocop-factory_bot (>= 2.0.0)
|
rubocop-factory_bot (>= 2.0.0)
|
||||||
rubocop-rspec (>= 2.25.0)
|
rubocop-rails (>= 2.25.0)
|
||||||
rubocop-factory_bot (2.25.1)
|
rubocop-rspec (>= 3.0.1)
|
||||||
rubocop (~> 1.41)
|
rubocop-rspec_rails (>= 2.30.0)
|
||||||
rubocop-rspec (2.27.1)
|
rubocop-factory_bot (2.26.1)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.61)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-rails (2.25.1)
|
||||||
rubocop-factory_bot (~> 2.22)
|
activesupport (>= 4.2.0)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
rubocop-rspec (3.0.2)
|
||||||
|
rubocop (~> 1.61)
|
||||||
|
rubocop-rspec_rails (2.30.0)
|
||||||
|
rubocop (~> 1.61)
|
||||||
|
rubocop-rspec (~> 3, >= 3.0.1)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
strscan (3.1.0)
|
strscan (3.1.0)
|
||||||
syntax_tree (6.2.0)
|
syntax_tree (6.2.0)
|
||||||
@@ -76,4 +85,4 @@ DEPENDENCIES
|
|||||||
syntax_tree
|
syntax_tree
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.4
|
2.5.10
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class DiscourseAiToolsNewRoute extends DiscourseRoute {
|
||||||
|
async model() {
|
||||||
|
return this.store.createRecord("ai-tool");
|
||||||
|
}
|
||||||
|
|
||||||
|
setupController(controller) {
|
||||||
|
super.setupController(...arguments);
|
||||||
|
const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools");
|
||||||
|
|
||||||
|
controller.set("allTools", toolsModel);
|
||||||
|
controller.set("presets", toolsModel.resultSetMeta.presets);
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class DiscourseAiToolsShowRoute extends DiscourseRoute {
|
||||||
|
async model(params) {
|
||||||
|
const allTools = this.modelFor("adminPlugins.show.discourse-ai-tools");
|
||||||
|
const id = parseInt(params.id, 10);
|
||||||
|
|
||||||
|
return allTools.find((tool) => tool.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupController(controller) {
|
||||||
|
super.setupController(...arguments);
|
||||||
|
const toolsModel = this.modelFor("adminPlugins.show.discourse-ai-tools");
|
||||||
|
|
||||||
|
controller.set("allTools", toolsModel);
|
||||||
|
controller.set("presets", toolsModel.resultSetMeta.presets);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { service } from "@ember/service";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class DiscourseAiToolsRoute extends DiscourseRoute {
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.store.findAll("ai-tool");
|
||||||
|
}
|
||||||
|
}
|
||||||
-6
@@ -1,9 +1,3 @@
|
|||||||
<DBreadcrumbsItem as |linkClass|>
|
|
||||||
<LinkTo @route="adminPlugins.show.discourse-ai-personas" class={{linkClass}}>
|
|
||||||
{{@model.plugin.nameTitleized}}
|
|
||||||
</LinkTo>
|
|
||||||
</DBreadcrumbsItem>
|
|
||||||
|
|
||||||
<AiPersonaListEditor
|
<AiPersonaListEditor
|
||||||
@personas={{this.allPersonas}}
|
@personas={{this.allPersonas}}
|
||||||
@currentPersona={{this.model}}
|
@currentPersona={{this.model}}
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<AiToolListEditor @tools={{this.model}} />
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<AiToolEditor
|
||||||
|
@tools={{this.allTools}}
|
||||||
|
@model={{this.model}}
|
||||||
|
@presets={{this.presets}}
|
||||||
|
/>
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<AiToolEditor
|
||||||
|
@tools={{this.allTools}}
|
||||||
|
@model={{this.model}}
|
||||||
|
@presets={{this.presets}}
|
||||||
|
/>
|
||||||
@@ -6,7 +6,7 @@ module DiscourseAi
|
|||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
def index
|
def index
|
||||||
llms = LlmModel.all
|
llms = LlmModel.all.order(:display_name)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
ai_llms:
|
ai_llms:
|
||||||
@@ -16,6 +16,8 @@ module DiscourseAi
|
|||||||
root: false,
|
root: false,
|
||||||
).as_json,
|
).as_json,
|
||||||
meta: {
|
meta: {
|
||||||
|
provider_params: LlmModel.provider_params,
|
||||||
|
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|
|
||||||
@@ -33,17 +35,48 @@ module DiscourseAi
|
|||||||
def create
|
def create
|
||||||
llm_model = LlmModel.new(ai_llm_params)
|
llm_model = LlmModel.new(ai_llm_params)
|
||||||
if llm_model.save
|
if llm_model.save
|
||||||
render json: { ai_persona: llm_model }, status: :created
|
llm_model.toggle_companion_user
|
||||||
|
render json: { ai_persona: LlmModelSerializer.new(llm_model) }, status: :created
|
||||||
else
|
else
|
||||||
render_json_error llm_model
|
render_json_error LlmModelSerializer.new(llm_model)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
llm_model = LlmModel.find(params[:id])
|
llm_model = LlmModel.find(params[:id])
|
||||||
|
|
||||||
if llm_model.update(ai_llm_params)
|
if llm_model.update(ai_llm_params(updating: llm_model))
|
||||||
render json: llm_model
|
llm_model.toggle_companion_user
|
||||||
|
render json: LlmModelSerializer.new(llm_model)
|
||||||
|
else
|
||||||
|
render_json_error LlmModelSerializer.new(llm_model)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
llm_model = LlmModel.find(params[:id])
|
||||||
|
|
||||||
|
in_use_by = DiscourseAi::Configuration::LlmValidator.new.modules_using(llm_model)
|
||||||
|
|
||||||
|
if !in_use_by.empty?
|
||||||
|
return(
|
||||||
|
render_json_error(
|
||||||
|
I18n.t(
|
||||||
|
"discourse_ai.llm.delete_failed",
|
||||||
|
settings: in_use_by.join(", "),
|
||||||
|
count: in_use_by.length,
|
||||||
|
),
|
||||||
|
status: 409,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clean up companion users
|
||||||
|
llm_model.enabled_chat_bot = false
|
||||||
|
llm_model.toggle_companion_user
|
||||||
|
|
||||||
|
if llm_model.destroy
|
||||||
|
head :no_content
|
||||||
else
|
else
|
||||||
render_json_error llm_model
|
render_json_error llm_model
|
||||||
end
|
end
|
||||||
@@ -54,11 +87,7 @@ module DiscourseAi
|
|||||||
|
|
||||||
llm_model = LlmModel.new(ai_llm_params)
|
llm_model = LlmModel.new(ai_llm_params)
|
||||||
|
|
||||||
DiscourseAi::Completions::Llm.proxy_from_obj(llm_model).generate(
|
DiscourseAi::Configuration::LlmValidator.new.run_test(llm_model)
|
||||||
"How much is 1 + 1?",
|
|
||||||
user: current_user,
|
|
||||||
feature_name: "llm_validator",
|
|
||||||
)
|
|
||||||
|
|
||||||
render json: { success: true }
|
render json: { success: true }
|
||||||
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed => e
|
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed => e
|
||||||
@@ -67,16 +96,32 @@ module DiscourseAi
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ai_llm_params
|
def ai_llm_params(updating: nil)
|
||||||
params.require(:ai_llm).permit(
|
permitted =
|
||||||
:display_name,
|
params.require(:ai_llm).permit(
|
||||||
:name,
|
:display_name,
|
||||||
:provider,
|
:name,
|
||||||
:tokenizer,
|
:provider,
|
||||||
:max_prompt_tokens,
|
:tokenizer,
|
||||||
:url,
|
:max_prompt_tokens,
|
||||||
:api_key,
|
:api_key,
|
||||||
)
|
:enabled_chat_bot,
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = updating ? updating.provider : permitted[:provider]
|
||||||
|
permit_url =
|
||||||
|
(updating && updating.url != LlmModel::RESERVED_VLLM_SRV_URL) ||
|
||||||
|
provider != LlmModel::BEDROCK_PROVIDER_NAME
|
||||||
|
|
||||||
|
permitted[:url] = params.dig(:ai_llm, :url) if permit_url
|
||||||
|
|
||||||
|
extra_field_names = LlmModel.provider_params.dig(provider&.to_sym, :fields).to_a
|
||||||
|
received_prov_params = params.dig(:ai_llm, :provider_params)
|
||||||
|
permitted[:provider_params] = received_prov_params.slice(
|
||||||
|
*extra_field_names,
|
||||||
|
).permit! if !extra_field_names.empty? && received_prov_params.present?
|
||||||
|
|
||||||
|
permitted
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,11 +19,19 @@ module DiscourseAi
|
|||||||
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
|
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
|
||||||
AiToolSerializer.new(tool, root: false)
|
AiToolSerializer.new(tool, root: false)
|
||||||
end
|
end
|
||||||
|
AiTool
|
||||||
|
.where(enabled: true)
|
||||||
|
.each do |tool|
|
||||||
|
tools << {
|
||||||
|
id: "custom-#{tool.id}",
|
||||||
|
name: I18n.t("discourse_ai.tools.custom_name", name: tool.name.capitalize),
|
||||||
|
}
|
||||||
|
end
|
||||||
llms =
|
llms =
|
||||||
DiscourseAi::Configuration::LlmEnumerator.values.map do |hash|
|
DiscourseAi::Configuration::LlmEnumerator.values.map do |hash|
|
||||||
{ id: hash[:value], name: hash[:name] }
|
{ id: hash[:value], name: hash[:name] }
|
||||||
end
|
end
|
||||||
render json: { ai_personas: ai_personas, meta: { commands: tools, llms: llms } }
|
render json: { ai_personas: ai_personas, meta: { tools: tools, llms: llms } }
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -35,7 +43,10 @@ module DiscourseAi
|
|||||||
if ai_persona.save
|
if ai_persona.save
|
||||||
RagDocumentFragment.link_persona_and_uploads(ai_persona, attached_upload_ids)
|
RagDocumentFragment.link_persona_and_uploads(ai_persona, attached_upload_ids)
|
||||||
|
|
||||||
render json: { ai_persona: ai_persona }, status: :created
|
render json: {
|
||||||
|
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
|
||||||
|
},
|
||||||
|
status: :created
|
||||||
else
|
else
|
||||||
render_json_error ai_persona
|
render_json_error ai_persona
|
||||||
end
|
end
|
||||||
@@ -50,9 +61,9 @@ module DiscourseAi
|
|||||||
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
|
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
|
||||||
RagDocumentFragment.update_persona_uploads(@ai_persona, attached_upload_ids)
|
RagDocumentFragment.update_persona_uploads(@ai_persona, attached_upload_ids)
|
||||||
|
|
||||||
render json: @ai_persona
|
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
||||||
else
|
else
|
||||||
render_json_error @ai_persona
|
render_json_error LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -60,7 +71,7 @@ module DiscourseAi
|
|||||||
if @ai_persona.destroy
|
if @ai_persona.destroy
|
||||||
head :no_content
|
head :no_content
|
||||||
else
|
else
|
||||||
render_json_error @ai_persona
|
render_json_error LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -126,28 +137,29 @@ module DiscourseAi
|
|||||||
:rag_conversation_chunks,
|
:rag_conversation_chunks,
|
||||||
:question_consolidator_llm,
|
:question_consolidator_llm,
|
||||||
:allow_chat,
|
:allow_chat,
|
||||||
|
:tool_details,
|
||||||
allowed_group_ids: [],
|
allowed_group_ids: [],
|
||||||
rag_uploads: [:id],
|
rag_uploads: [:id],
|
||||||
)
|
)
|
||||||
|
|
||||||
if commands = params.dig(:ai_persona, :commands)
|
if tools = params.dig(:ai_persona, :tools)
|
||||||
permitted[:commands] = permit_commands(commands)
|
permitted[:tools] = permit_tools(tools)
|
||||||
end
|
end
|
||||||
|
|
||||||
permitted
|
permitted
|
||||||
end
|
end
|
||||||
|
|
||||||
def permit_commands(commands)
|
def permit_tools(tools)
|
||||||
return [] if !commands.is_a?(Array)
|
return [] if !tools.is_a?(Array)
|
||||||
|
|
||||||
commands.filter_map do |command, options|
|
tools.filter_map do |tool, options|
|
||||||
break nil if !command.is_a?(String)
|
break nil if !tool.is_a?(String)
|
||||||
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
||||||
|
|
||||||
if options
|
if options
|
||||||
[command, options]
|
[tool, options]
|
||||||
else
|
else
|
||||||
command
|
tool
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Admin
|
||||||
|
class AiToolsController < ::Admin::AdminController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
|
before_action :find_ai_tool, only: %i[show update destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
ai_tools = AiTool.all
|
||||||
|
render_serialized({ ai_tools: ai_tools }, AiCustomToolListSerializer, root: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render_serialized(@ai_tool, AiCustomToolSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ai_tool = AiTool.new(ai_tool_params)
|
||||||
|
ai_tool.created_by_id = current_user.id
|
||||||
|
|
||||||
|
if ai_tool.save
|
||||||
|
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
|
||||||
|
else
|
||||||
|
render_json_error ai_tool
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @ai_tool.update(ai_tool_params)
|
||||||
|
render_serialized(@ai_tool, AiCustomToolSerializer)
|
||||||
|
else
|
||||||
|
render_json_error @ai_tool
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
if @ai_tool.destroy
|
||||||
|
head :no_content
|
||||||
|
else
|
||||||
|
render_json_error @ai_tool
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test
|
||||||
|
if params[:id].present?
|
||||||
|
ai_tool = AiTool.find(params[:id])
|
||||||
|
else
|
||||||
|
ai_tool = AiTool.new(ai_tool_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
parameters = params[:parameters].to_unsafe_h
|
||||||
|
|
||||||
|
# we need an llm so we have a tokenizer
|
||||||
|
# but will do without if none is available
|
||||||
|
llm = LlmModel.first&.to_llm
|
||||||
|
runner = ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {})
|
||||||
|
result = runner.invoke
|
||||||
|
|
||||||
|
if result.is_a?(Hash) && result[:error]
|
||||||
|
render_json_error result[:error]
|
||||||
|
else
|
||||||
|
render json: { output: result }
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound => e
|
||||||
|
render_json_error e.message, status: 400
|
||||||
|
rescue => e
|
||||||
|
render_json_error "Error executing the tool: #{e.message}", status: 400
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_ai_tool
|
||||||
|
@ai_tool = AiTool.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def ai_tool_params
|
||||||
|
params.require(:ai_tool).permit(
|
||||||
|
:name,
|
||||||
|
:description,
|
||||||
|
:script,
|
||||||
|
:summary,
|
||||||
|
parameters: [:name, :type, :description, :required, :enum, enum_values: []],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,7 +18,7 @@ module DiscourseAi
|
|||||||
|
|
||||||
debug_info = AiApiAuditLog.where(post: posts).order(created_at: :desc).first
|
debug_info = AiApiAuditLog.where(post: posts).order(created_at: :desc).first
|
||||||
|
|
||||||
render json: debug_info, status: 200
|
render json: AiApiAuditLogSerializer.new(debug_info, root: false), status: 200
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop_streaming_response
|
def stop_streaming_response
|
||||||
@@ -31,12 +31,10 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show_bot_username
|
def show_bot_username
|
||||||
bot_user_id = DiscourseAi::AiBot::EntryPoint.map_bot_model_to_user_id(params[:username])
|
bot_user = DiscourseAi::AiBot::EntryPoint.find_user_from_model(params[:username])
|
||||||
raise Discourse::InvalidParameters.new(:username) if !bot_user_id
|
raise Discourse::InvalidParameters.new(:username) if !bot_user
|
||||||
|
|
||||||
bot_username_lower = User.find(bot_user_id).username_lower
|
render json: { bot_username: bot_user.username_lower }, status: 200
|
||||||
|
|
||||||
render json: { bot_username: bot_username_lower }, status: 200
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module DiscourseAi
|
|||||||
requires_login only: %i[create update destroy]
|
requires_login only: %i[create update destroy]
|
||||||
before_action :require_site_settings!
|
before_action :require_site_settings!
|
||||||
|
|
||||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: %i[show]
|
skip_before_action :preload_json, :check_xhr, only: %i[show]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ensure_allowed_create!
|
ensure_allowed_create!
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ module DiscourseAi
|
|||||||
|
|
||||||
def suggest
|
def suggest
|
||||||
input = get_text_param!
|
input = get_text_param!
|
||||||
|
force_default_locale = params[:force_default_locale] || false
|
||||||
|
|
||||||
prompt = CompletionPrompt.find_by(id: params[:mode])
|
prompt = CompletionPrompt.find_by(id: params[:mode])
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ module DiscourseAi
|
|||||||
prompt,
|
prompt,
|
||||||
input,
|
input,
|
||||||
current_user,
|
current_user,
|
||||||
|
force_default_locale,
|
||||||
),
|
),
|
||||||
status: 200
|
status: 200
|
||||||
end
|
end
|
||||||
@@ -107,21 +109,6 @@ module DiscourseAi
|
|||||||
status: 502
|
status: 502
|
||||||
end
|
end
|
||||||
|
|
||||||
def random_caption
|
|
||||||
captions = [
|
|
||||||
"A beautiful landscape",
|
|
||||||
"An adorable puppy",
|
|
||||||
"A delicious meal",
|
|
||||||
"A cozy fireplace",
|
|
||||||
"A stunning sunset",
|
|
||||||
"A charming cityscape",
|
|
||||||
"A peaceful garden",
|
|
||||||
"A majestic mountain range",
|
|
||||||
"A captivating work of art",
|
|
||||||
]
|
|
||||||
captions.sample
|
|
||||||
end
|
|
||||||
|
|
||||||
def caption_image
|
def caption_image
|
||||||
image_url = params[:image_url]
|
image_url = params[:image_url]
|
||||||
image_url_type = params[:image_url_type]
|
image_url_type = params[:image_url_type]
|
||||||
@@ -138,19 +125,12 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
raise Discourse::NotFound if image.blank?
|
raise Discourse::NotFound if image.blank?
|
||||||
final_image_url = get_caption_url(image, image_url)
|
|
||||||
|
check_secure_upload_permission(image) if image.secure?
|
||||||
|
user = current_user
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
if Rails.env.development?
|
caption = DiscourseAi::AiHelper::Assistant.new.generate_image_caption(image, user)
|
||||||
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
|
|
||||||
render json: {
|
render json: {
|
||||||
caption:
|
caption:
|
||||||
"#{caption} (#{I18n.t("discourse_ai.ai_helper.image_caption.attribution")})",
|
"#{caption} (#{I18n.t("discourse_ai.ai_helper.image_caption.attribution")})",
|
||||||
@@ -181,15 +161,6 @@ module DiscourseAi
|
|||||||
raise Discourse::InvalidAccess
|
raise Discourse::InvalidAccess
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_caption_url(image_upload, image_url)
|
|
||||||
if image_upload.secure?
|
|
||||||
check_secure_upload_permission(image_upload)
|
|
||||||
return Discourse.store.url_for(image_upload)
|
|
||||||
end
|
|
||||||
|
|
||||||
UrlHelper.absolute(image_url)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Summarization
|
||||||
|
class ChatSummaryController < ::Chat::ApiController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
requires_plugin ::Chat::PLUGIN_NAME
|
||||||
|
|
||||||
|
VALID_SINCE_VALUES = [1, 3, 6, 12, 24, 72, 168]
|
||||||
|
|
||||||
|
def show
|
||||||
|
since = params[:since].to_i
|
||||||
|
raise Discourse::InvalidParameters.new(:since) if !VALID_SINCE_VALUES.include?(since)
|
||||||
|
|
||||||
|
channel = ::Chat::Channel.find(params[:channel_id])
|
||||||
|
guardian.ensure_can_join_chat_channel!(channel)
|
||||||
|
|
||||||
|
strategy = DiscourseAi::Summarization.default_strategy
|
||||||
|
raise Discourse::NotFound.new unless strategy
|
||||||
|
|
||||||
|
guardian.ensure_can_request_summary!
|
||||||
|
|
||||||
|
RateLimiter.new(current_user, "channel_summary", 6, 5.minutes).performed!
|
||||||
|
|
||||||
|
hijack do
|
||||||
|
content = { content_title: channel.name }
|
||||||
|
|
||||||
|
content[:contents] = channel
|
||||||
|
.chat_messages
|
||||||
|
.where("chat_messages.created_at > ?", since.hours.ago)
|
||||||
|
.includes(:user)
|
||||||
|
.order(created_at: :asc)
|
||||||
|
.pluck(:id, :username_lower, :message)
|
||||||
|
.map { { id: _1, poster: _2, text: _3 } }
|
||||||
|
|
||||||
|
summarized_text =
|
||||||
|
if content[:contents].empty?
|
||||||
|
I18n.t("discourse_ai.summarization.chat.no_targets")
|
||||||
|
else
|
||||||
|
strategy.summarize(content, current_user).dig(:summary)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { summary: summarized_text }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Summarization
|
||||||
|
class SummaryController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
|
def show
|
||||||
|
topic = Topic.find(params[:topic_id])
|
||||||
|
guardian.ensure_can_see!(topic)
|
||||||
|
|
||||||
|
raise Discourse::NotFound if !guardian.can_see_summary?(topic)
|
||||||
|
|
||||||
|
RateLimiter.new(current_user, "summary", 6, 5.minutes).performed! if current_user
|
||||||
|
|
||||||
|
opts = params.permit(:skip_age_check)
|
||||||
|
|
||||||
|
if params[:stream] && current_user
|
||||||
|
Jobs.enqueue(
|
||||||
|
:stream_topic_ai_summary,
|
||||||
|
topic_id: topic.id,
|
||||||
|
user_id: current_user.id,
|
||||||
|
opts: opts.as_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: success_json
|
||||||
|
else
|
||||||
|
hijack do
|
||||||
|
summary = DiscourseAi::TopicSummarization.summarize(topic, current_user, opts)
|
||||||
|
render_serialized(summary, AiTopicSummarySerializer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,7 +8,7 @@ module DiscourseAi
|
|||||||
VERSION = "1"
|
VERSION = "1"
|
||||||
|
|
||||||
def share_asset_url(short_path)
|
def share_asset_url(short_path)
|
||||||
::UrlHelper.local_cdn_url("/plugins/discourse-ai/ai-share/#{short_path}?#{VERSION}")
|
::UrlHelper.absolute("/plugins/discourse-ai/ai-share/#{short_path}?#{VERSION}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module Jobs
|
|||||||
|
|
||||||
topic = target.is_a?(Topic) ? target : target.topic
|
topic = target.is_a?(Topic) ? target : target.topic
|
||||||
post = target.is_a?(Post) ? target : target.first_post
|
post = target.is_a?(Post) ? target : target.first_post
|
||||||
return unless topic.present? && post.present?
|
return if topic.blank? || post.blank?
|
||||||
return if topic.private_message? && !SiteSetting.ai_embeddings_generate_for_pms
|
return if topic.private_message? && !SiteSetting.ai_embeddings_generate_for_pms
|
||||||
return if post.raw.blank?
|
return if post.raw.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class StreamTopicAiSummary < ::Jobs::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return unless topic = Topic.find_by(id: args[:topic_id])
|
||||||
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
|
|
||||||
|
strategy = DiscourseAi::Summarization.default_strategy
|
||||||
|
return if strategy.nil? || !Guardian.new(user).can_see_summary?(topic)
|
||||||
|
|
||||||
|
guardian = Guardian.new(user)
|
||||||
|
return unless guardian.can_see?(topic)
|
||||||
|
|
||||||
|
opts = args[:opts] || {}
|
||||||
|
|
||||||
|
streamed_summary = +""
|
||||||
|
start = Time.now
|
||||||
|
|
||||||
|
summary =
|
||||||
|
DiscourseAi::TopicSummarization
|
||||||
|
.new(strategy)
|
||||||
|
.summarize(topic, user, opts) do |partial_summary|
|
||||||
|
streamed_summary << partial_summary
|
||||||
|
|
||||||
|
# Throttle updates.
|
||||||
|
if (Time.now - start > 0.5) || Rails.env.test?
|
||||||
|
payload = { done: false, ai_topic_summary: { summarized_text: streamed_summary } }
|
||||||
|
|
||||||
|
publish_update(topic, user, payload)
|
||||||
|
start = Time.now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
publish_update(
|
||||||
|
topic,
|
||||||
|
user,
|
||||||
|
AiTopicSummarySerializer.new(summary, { scope: guardian }).as_json.merge(done: true),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def publish_update(topic, user, payload)
|
||||||
|
MessageBus.publish("/discourse-ai/summaries/topic/#{topic.id}", payload, user_ids: [user.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+94
-215
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiSummary < ActiveRecord::Base
|
||||||
|
belongs_to :target, polymorphic: true
|
||||||
|
|
||||||
|
def mark_as_outdated
|
||||||
|
@outdated = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def outdated
|
||||||
|
@outdated || false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: ai_summaries
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# target_id :integer not null
|
||||||
|
# target_type :string not null
|
||||||
|
# content_range :int4range
|
||||||
|
# summarized_text :string not null
|
||||||
|
# original_content_sha :string not null
|
||||||
|
# algorithm :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_ai_summaries_on_target_type_and_target_id (target_type,target_id)
|
||||||
|
#
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiTool < ActiveRecord::Base
|
||||||
|
validates :name, presence: true, length: { maximum: 100 }
|
||||||
|
validates :description, presence: true, length: { maximum: 1000 }
|
||||||
|
validates :summary, presence: true, length: { maximum: 255 }
|
||||||
|
validates :script, presence: true, length: { maximum: 100_000 }
|
||||||
|
validates :created_by_id, presence: true
|
||||||
|
belongs_to :created_by, class_name: "User"
|
||||||
|
|
||||||
|
def signature
|
||||||
|
{ name: name, description: description, parameters: parameters.map(&:symbolize_keys) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def runner(parameters, llm:, bot_user:, context: {})
|
||||||
|
DiscourseAi::AiBot::ToolRunner.new(
|
||||||
|
parameters: parameters,
|
||||||
|
llm: llm,
|
||||||
|
bot_user: bot_user,
|
||||||
|
context: context,
|
||||||
|
tool: self,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
after_commit :bump_persona_cache
|
||||||
|
|
||||||
|
def bump_persona_cache
|
||||||
|
AiPersona.persona_cache.flush!
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.presets
|
||||||
|
[
|
||||||
|
{
|
||||||
|
preset_id: "browse_web_jina",
|
||||||
|
name: "browse_web",
|
||||||
|
description: "Browse the web as a markdown document",
|
||||||
|
parameters: [
|
||||||
|
{ name: "url", type: "string", required: true, description: "The URL to browse" },
|
||||||
|
],
|
||||||
|
script: <<~SCRIPT,
|
||||||
|
let url;
|
||||||
|
function invoke(p) {
|
||||||
|
url = p.url;
|
||||||
|
result = http.get(`https://r.jina.ai/${url}`);
|
||||||
|
// truncates to 15000 tokens
|
||||||
|
return llm.truncate(result.body, 15000);
|
||||||
|
}
|
||||||
|
function details() {
|
||||||
|
return "Read: " + url
|
||||||
|
}
|
||||||
|
SCRIPT
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preset_id: "exchange_rate",
|
||||||
|
name: "exchange_rate",
|
||||||
|
description: "Get current exchange rates for various currencies",
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "base_currency",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
description: "The base currency code (e.g., USD, EUR)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target_currency",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
description: "The target currency code (e.g., EUR, JPY)",
|
||||||
|
},
|
||||||
|
{ name: "amount", type: "number", description: "Amount to convert eg: 123.45" },
|
||||||
|
],
|
||||||
|
script: <<~SCRIPT,
|
||||||
|
// note: this script uses the open.er-api.com service, it is only updated
|
||||||
|
// once every 24 hours, for more up to date rates see: https://www.exchangerate-api.com
|
||||||
|
function invoke(params) {
|
||||||
|
const url = `https://open.er-api.com/v6/latest/${params.base_currency}`;
|
||||||
|
const result = http.get(url);
|
||||||
|
if (result.status !== 200) {
|
||||||
|
return { error: "Failed to fetch exchange rates" };
|
||||||
|
}
|
||||||
|
const data = JSON.parse(result.body);
|
||||||
|
const rate = data.rates[params.target_currency];
|
||||||
|
if (!rate) {
|
||||||
|
return { error: "Target currency not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rval = {
|
||||||
|
base_currency: params.base_currency,
|
||||||
|
target_currency: params.target_currency,
|
||||||
|
exchange_rate: rate,
|
||||||
|
last_updated: data.time_last_update_utc
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.amount) {
|
||||||
|
rval.original_amount = params.amount;
|
||||||
|
rval.converted_amount = params.amount * rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rval;
|
||||||
|
}
|
||||||
|
|
||||||
|
function details() {
|
||||||
|
return "<a href='https://www.exchangerate-api.com'>Rates By Exchange Rate API</a>";
|
||||||
|
}
|
||||||
|
SCRIPT
|
||||||
|
summary: "Get current exchange rates between two currencies",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preset_id: "stock_quote",
|
||||||
|
name: "stock_quote",
|
||||||
|
description: "Get real-time stock quote information using AlphaVantage API",
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "symbol",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
description: "The stock symbol (e.g., AAPL, GOOGL)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
script: <<~SCRIPT,
|
||||||
|
function invoke(params) {
|
||||||
|
const apiKey = 'YOUR_ALPHAVANTAGE_API_KEY'; // Replace with your actual API key
|
||||||
|
const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${params.symbol}&apikey=${apiKey}`;
|
||||||
|
|
||||||
|
const result = http.get(url);
|
||||||
|
if (result.status !== 200) {
|
||||||
|
return { error: "Failed to fetch stock quote" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(result.body);
|
||||||
|
if (data['Error Message']) {
|
||||||
|
return { error: data['Error Message'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const quote = data['Global Quote'];
|
||||||
|
if (!quote || Object.keys(quote).length === 0) {
|
||||||
|
return { error: "No data found for the given symbol" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol: quote['01. symbol'],
|
||||||
|
price: parseFloat(quote['05. price']),
|
||||||
|
change: parseFloat(quote['09. change']),
|
||||||
|
change_percent: quote['10. change percent'],
|
||||||
|
volume: parseInt(quote['06. volume']),
|
||||||
|
latest_trading_day: quote['07. latest trading day']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function details() {
|
||||||
|
return "<a href='https://www.alphavantage.co'>Stock data provided by AlphaVantage</a>";
|
||||||
|
}
|
||||||
|
SCRIPT
|
||||||
|
summary: "Get real-time stock quotes using AlphaVantage API",
|
||||||
|
},
|
||||||
|
{ preset_id: "empty_tool", script: <<~SCRIPT },
|
||||||
|
function invoke(params) {
|
||||||
|
// logic here
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
function details() {
|
||||||
|
return "Details about this tool";
|
||||||
|
}
|
||||||
|
SCRIPT
|
||||||
|
].map do |preset|
|
||||||
|
preset[:preset_name] = I18n.t("discourse_ai.tools.presets.#{preset[:preset_id]}.name")
|
||||||
|
preset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: ai_tools
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# description :string not null
|
||||||
|
# summary :string not null
|
||||||
|
# parameters :jsonb not null
|
||||||
|
# script :text not null
|
||||||
|
# created_by_id :integer not null
|
||||||
|
# enabled :boolean default(TRUE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
@@ -6,7 +6,7 @@ end
|
|||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
#
|
#
|
||||||
# Table name: message_custom_prompts
|
# Table name: chat_message_custom_prompts
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# message_id :bigint not null
|
# message_id :bigint not null
|
||||||
@@ -16,5 +16,5 @@ end
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_message_custom_prompts_on_message_id (message_id) UNIQUE
|
# index_chat_message_custom_prompts_on_message_id (message_id) UNIQUE
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,9 +1,111 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class LlmModel < ActiveRecord::Base
|
class LlmModel < ActiveRecord::Base
|
||||||
|
FIRST_BOT_USER_ID = -1200
|
||||||
|
RESERVED_VLLM_SRV_URL = "https://vllm.shadowed-by-srv.invalid"
|
||||||
|
BEDROCK_PROVIDER_NAME = "aws_bedrock"
|
||||||
|
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
validates :url, exclusion: { in: [RESERVED_VLLM_SRV_URL] }, if: :url_changed?
|
||||||
|
|
||||||
|
def self.seed_srv_backed_model
|
||||||
|
srv = SiteSetting.ai_vllm_endpoint_srv
|
||||||
|
srv_model = find_by(url: RESERVED_VLLM_SRV_URL)
|
||||||
|
|
||||||
|
if srv.present?
|
||||||
|
if srv_model.present?
|
||||||
|
current_key = SiteSetting.ai_vllm_api_key
|
||||||
|
srv_model.update!(api_key: current_key) if current_key != srv_model.api_key
|
||||||
|
else
|
||||||
|
record =
|
||||||
|
new(
|
||||||
|
display_name: "vLLM SRV LLM",
|
||||||
|
name: "mistralai/Mixtral-8x7B-Instruct-v0.1",
|
||||||
|
provider: "vllm",
|
||||||
|
tokenizer: "DiscourseAi::Tokenizer::MixtralTokenizer",
|
||||||
|
url: RESERVED_VLLM_SRV_URL,
|
||||||
|
max_prompt_tokens: 8000,
|
||||||
|
api_key: SiteSetting.ai_vllm_api_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
record.save(validate: false) # Ignore reserved URL validation
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Clean up companion users
|
||||||
|
srv_model&.enabled_chat_bot = false
|
||||||
|
srv_model&.toggle_companion_user
|
||||||
|
|
||||||
|
srv_model&.destroy!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.provider_params
|
||||||
|
{
|
||||||
|
aws_bedrock: {
|
||||||
|
url_editable: false,
|
||||||
|
fields: %i[access_key_id region],
|
||||||
|
},
|
||||||
|
open_ai: {
|
||||||
|
url_editable: true,
|
||||||
|
fields: %i[organization],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_llm
|
||||||
|
DiscourseAi::Completions::Llm.proxy_from_obj(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle_companion_user
|
||||||
|
return if name == "fake" && Rails.env.production?
|
||||||
|
|
||||||
|
enable_check = SiteSetting.ai_bot_enabled && enabled_chat_bot
|
||||||
|
|
||||||
|
if enable_check
|
||||||
|
if !user
|
||||||
|
next_id = DB.query_single(<<~SQL).first
|
||||||
|
SELECT min(id) - 1 FROM users
|
||||||
|
SQL
|
||||||
|
|
||||||
|
new_user =
|
||||||
|
User.new(
|
||||||
|
id: [FIRST_BOT_USER_ID, next_id].min,
|
||||||
|
email: "no_email_#{SecureRandom.hex}",
|
||||||
|
name: name.titleize,
|
||||||
|
username: UserNameSuggester.suggest(name),
|
||||||
|
active: true,
|
||||||
|
approved: true,
|
||||||
|
admin: true,
|
||||||
|
moderator: true,
|
||||||
|
trust_level: TrustLevel[4],
|
||||||
|
)
|
||||||
|
new_user.save!(validate: false)
|
||||||
|
self.update!(user: new_user)
|
||||||
|
else
|
||||||
|
user.active = true
|
||||||
|
user.save!(validate: false)
|
||||||
|
end
|
||||||
|
elsif user
|
||||||
|
# will include deleted
|
||||||
|
has_posts = DB.query_single("SELECT 1 FROM posts WHERE user_id = #{user.id} LIMIT 1").present?
|
||||||
|
|
||||||
|
if has_posts
|
||||||
|
user.update!(active: false) if user.active
|
||||||
|
else
|
||||||
|
user.destroy!
|
||||||
|
self.update!(user: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def tokenizer_class
|
def tokenizer_class
|
||||||
tokenizer.constantize
|
tokenizer.constantize
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lookup_custom_param(key)
|
||||||
|
provider_params&.dig(key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
@@ -20,4 +122,6 @@ end
|
|||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# url :string
|
# url :string
|
||||||
# api_key :string
|
# api_key :string
|
||||||
|
# user_id :integer
|
||||||
|
# enabled_chat_bot :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
class ModelAccuracy < ActiveRecord::Base
|
class ModelAccuracy < ActiveRecord::Base
|
||||||
def self.adjust_model_accuracy(new_status, reviewable)
|
def self.adjust_model_accuracy(new_status, reviewable)
|
||||||
return unless %i[approved rejected].include?(new_status)
|
return if %i[approved rejected].exclude?(new_status)
|
||||||
return unless [ReviewableAiPost, ReviewableAiChatMessage].include?(reviewable.class)
|
return if [ReviewableAiPost, ReviewableAiChatMessage].exclude?(reviewable.class)
|
||||||
|
|
||||||
verdicts = reviewable.payload.to_h["verdicts"] || {}
|
verdicts = reviewable.payload.to_h["verdicts"] || {}
|
||||||
|
|
||||||
|
|||||||
@@ -133,12 +133,10 @@ class SharedAiConversation < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.build_conversation_data(topic, max_posts: DEFAULT_MAX_POSTS, include_usernames: false)
|
def self.build_conversation_data(topic, max_posts: DEFAULT_MAX_POSTS, include_usernames: false)
|
||||||
llm_name = nil
|
allowed_user_ids = topic.topic_allowed_users.pluck(:user_id)
|
||||||
topic.topic_allowed_users.each do |tu|
|
ai_bot_participant = DiscourseAi::AiBot::EntryPoint.find_participant_in(allowed_user_ids)
|
||||||
if DiscourseAi::AiBot::EntryPoint::BOT_USER_IDS.include?(tu.user_id)
|
|
||||||
llm_name = DiscourseAi::AiBot::EntryPoint.find_bot_by_id(tu.user_id)&.llm
|
llm_name = ai_bot_participant&.llm
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
llm_name = ActiveSupport::Inflector.humanize(llm_name) if llm_name
|
llm_name = ActiveSupport::Inflector.humanize(llm_name) if llm_name
|
||||||
llm_name ||= I18n.t("discourse_ai.unknown_model")
|
llm_name ||= I18n.t("discourse_ai.unknown_model")
|
||||||
@@ -170,9 +168,7 @@ class SharedAiConversation < ActiveRecord::Base
|
|||||||
cooked: post.cooked,
|
cooked: post.cooked,
|
||||||
}
|
}
|
||||||
|
|
||||||
mapped[:persona] = persona if ::DiscourseAi::AiBot::EntryPoint::BOT_USER_IDS.include?(
|
mapped[:persona] = persona if ai_bot_participant&.id == post.user_id
|
||||||
post.user_id,
|
|
||||||
)
|
|
||||||
mapped[:username] = post.user&.username if include_usernames
|
mapped[:username] = post.user&.username if include_usernames
|
||||||
mapped
|
mapped
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiApiAuditLogSerializer < ApplicationSerializer
|
||||||
|
attributes :id,
|
||||||
|
:provider_id,
|
||||||
|
:user_id,
|
||||||
|
:request_tokens,
|
||||||
|
:response_tokens,
|
||||||
|
:raw_request_payload,
|
||||||
|
:raw_response_payload,
|
||||||
|
:topic_id,
|
||||||
|
:post_id,
|
||||||
|
:feature_name,
|
||||||
|
:language_model,
|
||||||
|
:created_at
|
||||||
|
end
|
||||||
@@ -3,6 +3,17 @@
|
|||||||
class AiChatChannelSerializer < ApplicationSerializer
|
class AiChatChannelSerializer < ApplicationSerializer
|
||||||
attributes :id, :chatable, :chatable_type, :chatable_url, :slug
|
attributes :id, :chatable, :chatable_type, :chatable_url, :slug
|
||||||
|
|
||||||
|
def chatable
|
||||||
|
case object.chatable_type
|
||||||
|
when "Category"
|
||||||
|
BasicCategorySerializer.new(object.chatable, root: false).as_json
|
||||||
|
when "DirectMessage"
|
||||||
|
Chat::DirectMessageSerializer.new(object.chatable, scope: scope, root: false).as_json
|
||||||
|
when "Site"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def title
|
def title
|
||||||
# Display all participants for a DM.
|
# Display all participants for a DM.
|
||||||
# For category channels, the argument is ignored.
|
# For category channels, the argument is ignored.
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiCustomToolListSerializer < ApplicationSerializer
|
||||||
|
attributes :meta
|
||||||
|
|
||||||
|
has_many :ai_tools, serializer: AiCustomToolSerializer, embed: :objects
|
||||||
|
|
||||||
|
def meta
|
||||||
|
{ presets: AiTool.presets }
|
||||||
|
end
|
||||||
|
|
||||||
|
def ai_tools
|
||||||
|
object[:ai_tools]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiCustomToolSerializer < ApplicationSerializer
|
||||||
|
attributes :id,
|
||||||
|
:name,
|
||||||
|
:description,
|
||||||
|
:summary,
|
||||||
|
:parameters,
|
||||||
|
:script,
|
||||||
|
:created_by_id,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
|
||||||
|
self.root = "ai_tool"
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user