Compare commits
131
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059da39a4a | ||
|
|
22ccf2968f | ||
|
|
aef84bc5bb | ||
|
|
acd1986a5c | ||
|
|
100953319e | ||
|
|
67587367ab | ||
|
|
1573e6a694 | ||
|
|
5892e9e66c | ||
|
|
a49ed0cd01 | ||
|
|
cf45e6884c | ||
|
|
2a62658248 | ||
|
|
851ca57866 | ||
|
|
1b71330cea | ||
|
|
f090065405 | ||
|
|
c0e15501dd | ||
|
|
d62c76d1e8 | ||
|
|
f1b7cc8f7a | ||
|
|
e3d6dadc41 | ||
|
|
5bc9fdc06b | ||
|
|
adc2716cec | ||
|
|
c0a2d4c935 | ||
|
|
c6a307b473 | ||
|
|
e9fed4f5fa | ||
|
|
4eac377987 | ||
|
|
48305dc7d3 | ||
|
|
a6fa619c31 | ||
|
|
b2a6ee9c05 | ||
|
|
3800728d52 | ||
|
|
9196546f6f | ||
|
|
47d370588c | ||
|
|
a55f414667 | ||
|
|
491dac298f | ||
|
|
8b1b6811f4 | ||
|
|
fc3be6f0ce | ||
|
|
ab67299acb | ||
|
|
1e32416eaa | ||
|
|
81a664b3da | ||
|
|
7dc3c30fa4 | ||
|
|
8b90ce7c86 | ||
|
|
ede65c971f | ||
|
|
09a6841480 | ||
|
|
06ab446e47 | ||
|
|
541e6d7437 | ||
|
|
17f04c76d8 | ||
|
|
8669e8ae59 | ||
|
|
51a0516aa8 | ||
|
|
de0a52a0d9 | ||
|
|
b6630b670f | ||
|
|
81ef532268 | ||
|
|
cd0cfc0bfc | ||
|
|
60ea590ba5 | ||
|
|
10748015fd | ||
|
|
797d727a20 | ||
|
|
1a0d1d6e84 | ||
|
|
298ebee7dd | ||
|
|
b7b9179bc8 | ||
|
|
18dda31412 | ||
|
|
65718f6dbe | ||
|
|
2060426709 | ||
|
|
2a5c60db10 | ||
|
|
e1731dc3df | ||
|
|
4470e8af9b | ||
|
|
86f82ea4d3 | ||
|
|
d2002f81a7 | ||
|
|
e8b0f86300 | ||
|
|
244ec9d61e | ||
|
|
5fec8fe79e | ||
|
|
d26c7ac48d | ||
|
|
e2b0287333 | ||
|
|
0c8718e0d5 | ||
|
|
3440395623 | ||
|
|
b5d42f373b | ||
|
|
0f34ce999f | ||
|
|
274a54a324 | ||
|
|
fe7e73a6a4 | ||
|
|
e81108abb6 | ||
|
|
1300cc8a36 | ||
|
|
38b492529f | ||
|
|
67e3a610cb | ||
|
|
df63e36ad8 | ||
|
|
0024f2d264 | ||
|
|
ef08e5ea45 | ||
|
|
4de39a07e5 | ||
|
|
129ced9088 | ||
|
|
e15984029d | ||
|
|
0e4bf298b1 | ||
|
|
a2ffe9620a | ||
|
|
97d36524cd | ||
|
|
f9d641dd3a | ||
|
|
ed907dd004 | ||
|
|
5ed0990dfc | ||
|
|
aa5c968b18 | ||
|
|
fccd072f44 | ||
|
|
0d60aca6ef | ||
|
|
32da999144 | ||
|
|
8b573fe743 | ||
|
|
a14fe929a9 | ||
|
|
7b56d7d4fc | ||
|
|
f40e094945 | ||
|
|
bf5ccb452c | ||
|
|
5331b6dd8e | ||
|
|
fb68ae9698 | ||
|
|
30242a27e6 | ||
|
|
5b6d39a206 | ||
|
|
f3e78f0d80 | ||
|
|
28fa723472 | ||
|
|
f43137e067 | ||
|
|
da2b083025 | ||
|
|
6827d63e30 | ||
|
|
60fe924ce0 | ||
|
|
bab6f0be43 | ||
|
|
1b05da704e | ||
|
|
371146cdfe | ||
|
|
a6b08270c0 | ||
|
|
9a752eff29 | ||
|
|
913314b67d | ||
|
|
9dfae3d472 | ||
|
|
76a48786d9 | ||
|
|
f7536181af | ||
|
|
1dde82eb58 | ||
|
|
50e1bc774a | ||
|
|
2dd7068b46 | ||
|
|
7e4f93724d | ||
|
|
95160c80e3 | ||
|
|
2a8be6e2d7 | ||
|
|
0e2dd7378f | ||
|
|
01eced74a3 | ||
|
|
451f7662b2 | ||
|
|
75a1ade441 | ||
|
|
de8624416a | ||
|
|
b6483e416d |
@@ -1,3 +1,5 @@
|
|||||||
|
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
|
||||||
|
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
|
||||||
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
|
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
|
||||||
< 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87
|
< 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87
|
||||||
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
|
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
|
||||||
|
|||||||
@@ -5,3 +5,6 @@ node_modules
|
|||||||
evals/log
|
evals/log
|
||||||
evals/cases
|
evals/cases
|
||||||
config/eval-llms.local.yml
|
config/eval-llms.local.yml
|
||||||
|
# this gets rid of search results from ag, ripgrep, etc
|
||||||
|
tokenizers/
|
||||||
|
public/ai-share/highlight.min.js
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ GEM
|
|||||||
racc
|
racc
|
||||||
prettier_print (1.2.1)
|
prettier_print (1.2.1)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.12)
|
rack (3.1.14)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
rubocop (1.74.0)
|
rubocop (1.74.0)
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"tests": {
|
"tests": {
|
||||||
"requiredPlugins": [
|
"requiredPlugins": [
|
||||||
"https://github.com/discourse/discourse-prometheus"
|
"https://github.com/discourse/discourse-prometheus",
|
||||||
|
"https://github.com/discourse/discourse-solved.git"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import SiteSetting from "admin/models/site-setting";
|
||||||
|
|
||||||
|
export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRoute {
|
||||||
|
async model(params) {
|
||||||
|
const allFeatures = this.modelFor(
|
||||||
|
"adminPlugins.show.discourse-ai-features"
|
||||||
|
);
|
||||||
|
const id = parseInt(params.id, 10);
|
||||||
|
const currentFeature = allFeatures.find((feature) => feature.id === id);
|
||||||
|
|
||||||
|
const { site_settings } = await ajax("/admin/config/site_settings.json", {
|
||||||
|
data: {
|
||||||
|
filter_area: `ai-features/${currentFeature.ref}`,
|
||||||
|
plugin: "discourse-ai",
|
||||||
|
category: "discourse_ai",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
currentFeature.feature_settings = site_settings.map((setting) =>
|
||||||
|
SiteSetting.create(setting)
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentFeature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { service } from "@ember/service";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class AdminPluginsShowDiscourseAiFeatures extends DiscourseRoute {
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
async model() {
|
||||||
|
return this.store.findAll("ai-feature");
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -2,8 +2,17 @@ import DiscourseRoute from "discourse/routes/discourse";
|
|||||||
|
|
||||||
export default class AdminPluginsShowDiscourseAiLlmsEdit extends DiscourseRoute {
|
export default class AdminPluginsShowDiscourseAiLlmsEdit extends DiscourseRoute {
|
||||||
async model(params) {
|
async model(params) {
|
||||||
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
|
|
||||||
const id = parseInt(params.id, 10);
|
const id = parseInt(params.id, 10);
|
||||||
|
|
||||||
|
if (id < 0) {
|
||||||
|
// You shouldn't be able to access the edit page
|
||||||
|
// if the model is seeded
|
||||||
|
return this.router.transitionTo(
|
||||||
|
"adminPlugins.show.discourse-ai-llms.index"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
|
||||||
const record = allLlms.findBy("id", id);
|
const record = allLlms.findBy("id", id);
|
||||||
record.provider_params = record.provider_params || {};
|
record.provider_params = record.provider_params || {};
|
||||||
return record;
|
return record;
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import RouteTemplate from "ember-route-template";
|
||||||
|
import BackButton from "discourse/components/back-button";
|
||||||
|
import SiteSettingComponent from "admin/components/site-setting";
|
||||||
|
|
||||||
|
export default RouteTemplate(
|
||||||
|
<template>
|
||||||
|
<BackButton
|
||||||
|
@route="adminPlugins.show.discourse-ai-features"
|
||||||
|
@label="discourse_ai.features.back"
|
||||||
|
/>
|
||||||
|
<section class="ai-feature-editor__header">
|
||||||
|
<h2>{{@model.name}}</h2>
|
||||||
|
<p>{{@model.description}}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ai-feature-editor">
|
||||||
|
{{#each @model.feature_settings as |setting|}}
|
||||||
|
<div>
|
||||||
|
<SiteSettingComponent @setting={{setting}} />
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import RouteTemplate from "ember-route-template";
|
||||||
|
import { gt } from "truth-helpers";
|
||||||
|
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
|
export default RouteTemplate(
|
||||||
|
class extends Component {
|
||||||
|
@service adminPluginNavManager;
|
||||||
|
|
||||||
|
get tableHeaders() {
|
||||||
|
const prefix = "discourse_ai.features.list.header";
|
||||||
|
return [
|
||||||
|
i18n(`${prefix}.name`),
|
||||||
|
i18n(`${prefix}.persona`),
|
||||||
|
i18n(`${prefix}.groups`),
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get configuredFeatures() {
|
||||||
|
return this.args.model.filter(
|
||||||
|
(feature) => feature.enable_setting.value === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get unconfiguredFeatures() {
|
||||||
|
return this.args.model.filter(
|
||||||
|
(feature) => feature.enable_setting.value === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DBreadcrumbsItem
|
||||||
|
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-features"
|
||||||
|
@label={{i18n "discourse_ai.features.short_title"}}
|
||||||
|
/>
|
||||||
|
<section class="ai-feature-list admin-detail">
|
||||||
|
<DPageSubheader
|
||||||
|
@titleLabel={{i18n "discourse_ai.features.short_title"}}
|
||||||
|
@descriptionLabel={{i18n "discourse_ai.features.description"}}
|
||||||
|
@learnMoreUrl="todo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if (gt this.configuredFeatures.length 0)}}
|
||||||
|
<div class="ai-feature-list__configured-features">
|
||||||
|
<h3>{{i18n "discourse_ai.features.list.configured_features"}}</h3>
|
||||||
|
|
||||||
|
<table class="d-admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{{#each this.tableHeaders as |header|}}
|
||||||
|
<th>{{header}}</th>
|
||||||
|
{{/each}}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{{#each this.configuredFeatures as |feature|}}
|
||||||
|
<tr
|
||||||
|
class="ai-feature-list__row d-admin-row__content"
|
||||||
|
data-feature-name={{feature.name}}
|
||||||
|
>
|
||||||
|
<td class="d-admin-row__overview ai-feature-list__row-item">
|
||||||
|
<span class="ai-feature-list__row-item-name">
|
||||||
|
<strong>
|
||||||
|
{{feature.name}}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<span class="ai-feature-list__row-item-description">
|
||||||
|
{{feature.description}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__persona"
|
||||||
|
>
|
||||||
|
<DButton
|
||||||
|
class="btn-flat btn-small ai-feature-list__row-item-persona"
|
||||||
|
@translatedLabel={{feature.persona.name}}
|
||||||
|
@route="adminPlugins.show.discourse-ai-personas.edit"
|
||||||
|
@routeModels={{feature.persona.id}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups"
|
||||||
|
>
|
||||||
|
{{#if (gt feature.persona.allowed_groups.length 0)}}
|
||||||
|
<ul class="ai-feature-list__row-item-groups">
|
||||||
|
{{#each feature.persona.allowed_groups as |group|}}
|
||||||
|
<li>{{group.name}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
<td class="d-admin-row_controls">
|
||||||
|
<DButton
|
||||||
|
class="btn-small edit"
|
||||||
|
@label="discourse_ai.features.list.edit"
|
||||||
|
@route="adminPlugins.show.discourse-ai-features.edit"
|
||||||
|
@routeModels={{feature.id}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (gt this.unconfiguredFeatures.length 0)}}
|
||||||
|
<div class="ai-feature-list__unconfigured-features">
|
||||||
|
<h3>{{i18n "discourse_ai.features.list.unconfigured_features"}}</h3>
|
||||||
|
|
||||||
|
<table class="d-admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{i18n "discourse_ai.features.list.header.name"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{{#each this.unconfiguredFeatures as |feature|}}
|
||||||
|
<tr class="ai-feature-list__row d-admin-row__content">
|
||||||
|
<td class="d-admin-row__overview ai-feature-list__row-item">
|
||||||
|
<span class="ai-feature-list__row-item-name">
|
||||||
|
<strong>
|
||||||
|
{{feature.name}}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<span class="ai-feature-list__row-item-description">
|
||||||
|
{{feature.description}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="d-admin-row_controls">
|
||||||
|
<DButton
|
||||||
|
class="btn-small"
|
||||||
|
@label="discourse_ai.features.list.set_up"
|
||||||
|
@route="adminPlugins.show.discourse-ai-features.edit"
|
||||||
|
@routeModels={{feature.id}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Admin
|
||||||
|
class AiFeaturesController < ::Admin::AdminController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: serialize_features(DiscourseAi::Features.features)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
|
||||||
|
render json: serialize_feature(DiscourseAi::Features.find_feature_by_id(params[:id].to_i))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialize_features(features)
|
||||||
|
features.map { |feature| feature.merge(persona: serialize_persona(feature[:persona])) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_feature(feature)
|
||||||
|
return nil if feature.blank?
|
||||||
|
|
||||||
|
feature.merge(persona: serialize_persona(feature[:persona]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_persona(persona)
|
||||||
|
return nil if persona.blank?
|
||||||
|
|
||||||
|
serialize_data(persona, AiFeaturesPersonaSerializer, root: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -157,9 +157,13 @@ module DiscourseAi
|
|||||||
:provider,
|
:provider,
|
||||||
:tokenizer,
|
:tokenizer,
|
||||||
:max_prompt_tokens,
|
:max_prompt_tokens,
|
||||||
|
:max_output_tokens,
|
||||||
:api_key,
|
:api_key,
|
||||||
:enabled_chat_bot,
|
:enabled_chat_bot,
|
||||||
:vision_enabled,
|
:vision_enabled,
|
||||||
|
:input_cost,
|
||||||
|
:cached_input_cost,
|
||||||
|
:output_cost,
|
||||||
)
|
)
|
||||||
|
|
||||||
provider = updating ? updating.provider : permitted[:provider]
|
provider = updating ? updating.provider : permitted[:provider]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module DiscourseAi
|
|||||||
LocalizedAiPersonaSerializer.new(persona, root: false)
|
LocalizedAiPersonaSerializer.new(persona, root: false)
|
||||||
end
|
end
|
||||||
tools =
|
tools =
|
||||||
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
|
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
|
||||||
AiToolSerializer.new(tool, root: false)
|
AiToolSerializer.new(tool, root: false)
|
||||||
end
|
end
|
||||||
AiTool
|
AiTool
|
||||||
@@ -221,6 +221,14 @@ module DiscourseAi
|
|||||||
permitted[:tools] = permit_tools(tools)
|
permitted[:tools] = permit_tools(tools)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if response_format = params.dig(:ai_persona, :response_format)
|
||||||
|
permitted[:response_format] = permit_response_format(response_format)
|
||||||
|
end
|
||||||
|
|
||||||
|
if examples = params.dig(:ai_persona, :examples)
|
||||||
|
permitted[:examples] = permit_examples(examples)
|
||||||
|
end
|
||||||
|
|
||||||
permitted
|
permitted
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -235,6 +243,24 @@ module DiscourseAi
|
|||||||
[tool, options, !!force_tool]
|
[tool, options, !!force_tool]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def permit_response_format(response_format)
|
||||||
|
return [] if !response_format.is_a?(Array)
|
||||||
|
|
||||||
|
response_format.map do |element|
|
||||||
|
if element && element.is_a?(ActionController::Parameters)
|
||||||
|
element.permit!
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def permit_examples(examples)
|
||||||
|
return [] if !examples.is_a?(Array)
|
||||||
|
|
||||||
|
examples.map { |example_arr| example_arr.take(2).map(&:to_s) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ module DiscourseAi
|
|||||||
# we need an llm so we have a tokenizer
|
# we need an llm so we have a tokenizer
|
||||||
# but will do without if none is available
|
# but will do without if none is available
|
||||||
llm = LlmModel.first&.to_llm
|
llm = LlmModel.first&.to_llm
|
||||||
runner = @ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {})
|
runner = @ai_tool.runner(parameters, llm: llm, bot_user: current_user)
|
||||||
result = runner.invoke
|
result = runner.invoke
|
||||||
|
|
||||||
if result.is_a?(Hash) && result[:error]
|
if result.is_a?(Hash) && result[:error]
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ module DiscourseAi
|
|||||||
|
|
||||||
def discover
|
def discover
|
||||||
ai_persona =
|
ai_persona =
|
||||||
AiPersona.all_personas.find do |persona|
|
AiPersona
|
||||||
persona.id == SiteSetting.ai_bot_discover_persona.to_i
|
.all_personas(enabled_only: false)
|
||||||
end
|
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
|
||||||
|
|
||||||
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
|
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
|
||||||
raise Discourse::InvalidAccess.new
|
raise Discourse::InvalidAccess.new
|
||||||
@@ -69,6 +69,40 @@ module DiscourseAi
|
|||||||
|
|
||||||
render json: {}, status: 200
|
render json: {}, status: 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def discover_continue_convo
|
||||||
|
raise Discourse::InvalidParameters.new("user_id") if !params[:user_id]
|
||||||
|
raise Discourse::InvalidParameters.new("query") if !params[:query]
|
||||||
|
raise Discourse::InvalidParameters.new("context") if !params[:context]
|
||||||
|
|
||||||
|
user = User.find(params[:user_id])
|
||||||
|
|
||||||
|
bot_user_id = AiPersona.find_by(id: SiteSetting.ai_bot_discover_persona).user_id
|
||||||
|
bot_username = User.find_by(id: bot_user_id).username
|
||||||
|
|
||||||
|
query = params[:query]
|
||||||
|
context = "[quote]\n#{params[:context]}\n[/quote]"
|
||||||
|
|
||||||
|
post =
|
||||||
|
PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title:
|
||||||
|
I18n.t("discourse_ai.ai_bot.discoveries.continue_conversation.title", query: query),
|
||||||
|
raw:
|
||||||
|
I18n.t(
|
||||||
|
"discourse_ai.ai_bot.discoveries.continue_conversation.raw",
|
||||||
|
query: query,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
archetype: Archetype.private_message,
|
||||||
|
target_usernames: bot_username,
|
||||||
|
skip_validations: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: success_json.merge(topic_id: post.topic_id)
|
||||||
|
rescue StandardError => e
|
||||||
|
render json: failed_json.merge(errors: [e.message]), status: 422
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module AiBot
|
||||||
|
class ConversationsController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def index
|
||||||
|
page = params[:page].to_i
|
||||||
|
per_page = params[:per_page]&.to_i || 40
|
||||||
|
|
||||||
|
base_query =
|
||||||
|
Topic
|
||||||
|
.private_messages_for_user(current_user)
|
||||||
|
.where(user: current_user) # Only show PMs where the current user is the author
|
||||||
|
.joins(
|
||||||
|
"INNER JOIN topic_custom_fields tcf ON tcf.topic_id = topics.id
|
||||||
|
AND tcf.name = '#{DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD}'
|
||||||
|
AND tcf.value = 't'",
|
||||||
|
)
|
||||||
|
.distinct
|
||||||
|
|
||||||
|
total = base_query.count
|
||||||
|
pms = base_query.order(last_posted_at: :desc).offset(page * per_page).limit(per_page)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
conversations: serialize_data(pms, ListableTopicSerializer),
|
||||||
|
meta: {
|
||||||
|
total: total,
|
||||||
|
page: page,
|
||||||
|
per_page: per_page,
|
||||||
|
has_more: total > (page + 1) * per_page,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -43,7 +43,7 @@ module DiscourseAi
|
|||||||
prompt,
|
prompt,
|
||||||
input,
|
input,
|
||||||
current_user,
|
current_user,
|
||||||
force_default_locale,
|
force_default_locale: force_default_locale,
|
||||||
),
|
),
|
||||||
status: 200
|
status: 200
|
||||||
end
|
end
|
||||||
@@ -110,26 +110,44 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def stream_suggestion
|
def stream_suggestion
|
||||||
post_id = get_post_param!
|
|
||||||
text = get_text_param!
|
text = get_text_param!
|
||||||
post = Post.includes(:topic).find_by(id: post_id)
|
|
||||||
|
location = params[:location]
|
||||||
|
raise Discourse::InvalidParameters.new(:location) if !location
|
||||||
|
|
||||||
prompt = CompletionPrompt.find_by(id: params[:mode])
|
prompt = CompletionPrompt.find_by(id: params[:mode])
|
||||||
|
|
||||||
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
||||||
raise Discourse::InvalidParameters.new(:post_id) unless post
|
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||||
|
|
||||||
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
||||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
Jobs.enqueue(
|
if location == "composer"
|
||||||
:stream_post_helper,
|
Jobs.enqueue(
|
||||||
post_id: post.id,
|
:stream_composer_helper,
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
text: text,
|
text: text,
|
||||||
prompt: prompt.name,
|
prompt: prompt.name,
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
)
|
force_default_locale: params[:force_default_locale] || false,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
post_id = get_post_param!
|
||||||
|
post = Post.includes(:topic).find_by(id: post_id)
|
||||||
|
|
||||||
|
raise Discourse::InvalidParameters.new(:post_id) unless post
|
||||||
|
|
||||||
|
Jobs.enqueue(
|
||||||
|
:stream_post_helper,
|
||||||
|
post_id: post.id,
|
||||||
|
user_id: current_user.id,
|
||||||
|
text: text,
|
||||||
|
prompt: prompt.name,
|
||||||
|
custom_prompt: params[:custom_prompt],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
render json: { success: true }, status: 200
|
render json: { success: true }, status: 200
|
||||||
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed
|
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed
|
||||||
|
|||||||
@@ -46,11 +46,19 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
semantic_search
|
begin
|
||||||
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
|
semantic_search
|
||||||
.each { |topic_post| grouped_results.add(topic_post) }
|
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
|
||||||
|
.each { |topic_post| grouped_results.add(topic_post) }
|
||||||
|
|
||||||
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
|
render_serialized(
|
||||||
|
grouped_results,
|
||||||
|
GroupedSearchResultSerializer,
|
||||||
|
result: grouped_results,
|
||||||
|
)
|
||||||
|
rescue Discourse::InvalidAccess
|
||||||
|
render_json_error(I18n.t("invalid_access"), status: 403)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ module DiscourseAi
|
|||||||
u.name,
|
u.name,
|
||||||
u.uploaded_avatar_id,
|
u.uploaded_avatar_id,
|
||||||
c.id AS category_id,
|
c.id AS category_id,
|
||||||
c.name AS category_name,
|
|
||||||
c.color AS category_color,
|
|
||||||
c.slug AS category_slug,
|
|
||||||
c.description AS category_description,
|
|
||||||
(CASE
|
(CASE
|
||||||
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
|
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
|
||||||
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
|
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ module ::Jobs
|
|||||||
return if message.blank?
|
return if message.blank?
|
||||||
|
|
||||||
personaClass =
|
personaClass =
|
||||||
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
DiscourseAi::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
||||||
return if personaClass.blank?
|
return if personaClass.blank?
|
||||||
|
|
||||||
user = User.find_by(id: personaClass.user_id)
|
user = User.find_by(id: personaClass.user_id)
|
||||||
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
|
bot = DiscourseAi::Personas::Bot.as(user, persona: personaClass.new)
|
||||||
|
|
||||||
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
|
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ module ::Jobs
|
|||||||
persona_id = args[:persona_id]
|
persona_id = args[:persona_id]
|
||||||
|
|
||||||
begin
|
begin
|
||||||
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id)
|
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
|
||||||
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
|
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
|
||||||
|
|
||||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
|
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
||||||
|
|
||||||
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
|
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
|
||||||
rescue DiscourseAi::AiBot::Bot::BOT_NOT_FOUND
|
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
|
||||||
Rails.logger.warn(
|
Rails.logger.warn(
|
||||||
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class StreamComposerHelper < ::Jobs::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return unless args[:prompt]
|
||||||
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
|
return unless args[:text]
|
||||||
|
|
||||||
|
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
||||||
|
|
||||||
|
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
||||||
|
prompt.custom_instruction = args[:custom_prompt]
|
||||||
|
end
|
||||||
|
|
||||||
|
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
||||||
|
prompt,
|
||||||
|
args[:text],
|
||||||
|
user,
|
||||||
|
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||||
|
force_default_locale: args[:force_default_locale],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,8 @@ module Jobs
|
|||||||
def execute(args)
|
def execute(args)
|
||||||
interaction = args[:interaction]
|
interaction = args[:interaction]
|
||||||
|
|
||||||
|
return unless SiteSetting.ai_discord_search_enabled
|
||||||
|
|
||||||
if SiteSetting.ai_discord_search_mode == "persona"
|
if SiteSetting.ai_discord_search_mode == "persona"
|
||||||
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
|
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ module Jobs
|
|||||||
return if (query = args[:query]).blank?
|
return if (query = args[:query]).blank?
|
||||||
|
|
||||||
ai_persona_klass =
|
ai_persona_klass =
|
||||||
AiPersona.all_personas.find do |persona|
|
AiPersona
|
||||||
persona.id == SiteSetting.ai_bot_discover_persona.to_i
|
.all_personas(enabled_only: false)
|
||||||
end
|
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
|
||||||
|
|
||||||
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
|
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
|
||||||
return
|
return
|
||||||
@@ -19,7 +19,7 @@ module Jobs
|
|||||||
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
|
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
|
||||||
|
|
||||||
bot =
|
bot =
|
||||||
DiscourseAi::AiBot::Bot.as(
|
DiscourseAi::Personas::Bot.as(
|
||||||
Discourse.system_user,
|
Discourse.system_user,
|
||||||
persona: ai_persona_klass.new,
|
persona: ai_persona_klass.new,
|
||||||
model: llm_model,
|
model: llm_model,
|
||||||
@@ -30,9 +30,13 @@ module Jobs
|
|||||||
|
|
||||||
base = { query: query, model_used: llm_model.display_name }
|
base = { query: query, model_used: llm_model.display_name }
|
||||||
|
|
||||||
bot.reply(
|
context =
|
||||||
{ conversation_context: [{ type: :user, content: query }], skip_tool_details: true },
|
DiscourseAi::Personas::BotContext.new(
|
||||||
) do |partial|
|
messages: [{ type: :user, content: query }],
|
||||||
|
skip_tool_details: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
bot.reply(context) do |partial|
|
||||||
streamed_reply << partial
|
streamed_reply << partial
|
||||||
|
|
||||||
# Throttle updates.
|
# Throttle updates.
|
||||||
|
|||||||
+54
-16
@@ -13,6 +13,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
validate :system_persona_unchangeable, on: :update, if: :system
|
validate :system_persona_unchangeable, on: :update, if: :system
|
||||||
validate :chat_preconditions
|
validate :chat_preconditions
|
||||||
validate :allowed_seeded_model, if: :default_llm_id
|
validate :allowed_seeded_model, if: :default_llm_id
|
||||||
|
validate :well_formated_examples
|
||||||
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
||||||
# leaves some room for growth but sets a maximum to avoid memory issues
|
# leaves some room for growth but sets a maximum to avoid memory issues
|
||||||
# we may want to revisit this in the future
|
# we may want to revisit this in the future
|
||||||
@@ -46,13 +47,18 @@ class AiPersona < ActiveRecord::Base
|
|||||||
|
|
||||||
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
|
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
|
||||||
|
|
||||||
def self.all_personas
|
def self.all_personas(enabled_only: true)
|
||||||
persona_cache[:value] ||= AiPersona
|
persona_cache[:value] ||= AiPersona
|
||||||
.ordered
|
.ordered
|
||||||
.where(enabled: true)
|
|
||||||
.all
|
.all
|
||||||
.limit(MAX_PERSONAS_PER_SITE)
|
.limit(MAX_PERSONAS_PER_SITE)
|
||||||
.map(&:class_instance)
|
.map(&:class_instance)
|
||||||
|
|
||||||
|
if enabled_only
|
||||||
|
persona_cache[:value].select { |p| p.enabled }
|
||||||
|
else
|
||||||
|
persona_cache[:value]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.persona_users(user: nil)
|
def self.persona_users(user: nil)
|
||||||
@@ -176,6 +182,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
description
|
description
|
||||||
allowed_group_ids
|
allowed_group_ids
|
||||||
tool_details
|
tool_details
|
||||||
|
enabled
|
||||||
]
|
]
|
||||||
|
|
||||||
instance_attributes = {}
|
instance_attributes = {}
|
||||||
@@ -201,14 +208,14 @@ class AiPersona < ActiveRecord::Base
|
|||||||
if inner_name.start_with?("custom-")
|
if inner_name.start_with?("custom-")
|
||||||
custom_tool_id = inner_name.split("-", 2).last.to_i
|
custom_tool_id = inner_name.split("-", 2).last.to_i
|
||||||
if AiTool.exists?(id: custom_tool_id, enabled: true)
|
if AiTool.exists?(id: custom_tool_id, enabled: true)
|
||||||
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
|
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
inner_name = inner_name.gsub("Tool", "")
|
inner_name = inner_name.gsub("Tool", "")
|
||||||
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize
|
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize
|
||||||
options[klass] = current_options if current_options
|
options[klass] = current_options if current_options
|
||||||
rescue StandardError
|
rescue StandardError
|
||||||
end
|
end
|
||||||
@@ -218,19 +225,26 @@ class AiPersona < ActiveRecord::Base
|
|||||||
klass
|
klass
|
||||||
end
|
end
|
||||||
|
|
||||||
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
|
||||||
if persona_class
|
if persona_class
|
||||||
instance_attributes.each do |key, value|
|
return(
|
||||||
# description/name are localized
|
# we need a new copy so we don't leak information
|
||||||
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
|
# across sites
|
||||||
end
|
Class.new(persona_class) do
|
||||||
persona_class.define_method(:options) { options }
|
# required for localization
|
||||||
return persona_class
|
define_singleton_method(:to_s) { persona_class.to_s }
|
||||||
|
instance_attributes.each do |key, value|
|
||||||
|
# description/name are localized
|
||||||
|
define_singleton_method(key) { value } if key != :description && key != :name
|
||||||
|
end
|
||||||
|
define_method(:options) { options }
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
ai_persona_id = self.id
|
ai_persona_id = self.id
|
||||||
|
|
||||||
Class.new(DiscourseAi::AiBot::Personas::Persona) do
|
Class.new(DiscourseAi::Personas::Persona) do
|
||||||
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
|
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
|
||||||
|
|
||||||
define_singleton_method(:to_s) do
|
define_singleton_method(:to_s) do
|
||||||
@@ -252,6 +266,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
define_method(:top_p) { @ai_persona&.top_p }
|
define_method(:top_p) { @ai_persona&.top_p }
|
||||||
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
||||||
define_method(:uploads) { @ai_persona&.uploads }
|
define_method(:uploads) { @ai_persona&.uploads }
|
||||||
|
define_method(:examples) { @ai_persona&.examples }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -312,9 +327,11 @@ class AiPersona < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def system_persona_unchangeable
|
def system_persona_unchangeable
|
||||||
|
error_msg = I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona")
|
||||||
|
|
||||||
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
|
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
|
||||||
description_changed?
|
description_changed?
|
||||||
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
|
errors.add(:base, error_msg)
|
||||||
elsif tools_changed?
|
elsif tools_changed?
|
||||||
old_tools = tools_change[0]
|
old_tools = tools_change[0]
|
||||||
new_tools = tools_change[1]
|
new_tools = tools_change[1]
|
||||||
@@ -322,9 +339,17 @@ class AiPersona < ActiveRecord::Base
|
|||||||
old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
|
old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
|
||||||
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
|
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
|
||||||
|
|
||||||
if old_tool_names != new_tool_names
|
errors.add(:base, error_msg) if old_tool_names != new_tool_names
|
||||||
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
|
elsif response_format_changed?
|
||||||
end
|
old_format = response_format_change[0].map { |f| f["key"] }.to_set
|
||||||
|
new_format = response_format_change[1].map { |f| f["key"] }.to_set
|
||||||
|
|
||||||
|
errors.add(:base, error_msg) if old_format != new_format
|
||||||
|
elsif examples_changed?
|
||||||
|
old_examples = examples_change[0].flatten.to_set
|
||||||
|
new_examples = examples_change[1].flatten.to_set
|
||||||
|
|
||||||
|
errors.add(:base, error_msg) if old_examples != new_examples
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -345,6 +370,17 @@ class AiPersona < ActiveRecord::Base
|
|||||||
|
|
||||||
errors.add(:default_llm, I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"))
|
errors.add(:default_llm, I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def well_formated_examples
|
||||||
|
return if examples.blank?
|
||||||
|
|
||||||
|
if examples.is_a?(Array) &&
|
||||||
|
examples.all? { |e| e.is_a?(Array) && e.length == 2 && e.all?(&:present?) }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
errors.add(:examples, I18n.t("discourse_ai.personas.malformed_examples"))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
@@ -382,6 +418,8 @@ end
|
|||||||
# rag_llm_model_id :bigint
|
# rag_llm_model_id :bigint
|
||||||
# default_llm_id :bigint
|
# default_llm_id :bigint
|
||||||
# question_consolidator_llm_id :bigint
|
# question_consolidator_llm_id :bigint
|
||||||
|
# response_format :jsonb
|
||||||
|
# examples :jsonb
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
+162
-82
@@ -35,8 +35,8 @@ class AiTool < ActiveRecord::Base
|
|||||||
tool_name.presence || name
|
tool_name.presence || name
|
||||||
end
|
end
|
||||||
|
|
||||||
def runner(parameters, llm:, bot_user:, context: {})
|
def runner(parameters, llm:, bot_user:, context: nil)
|
||||||
DiscourseAi::AiBot::ToolRunner.new(
|
DiscourseAi::Personas::ToolRunner.new(
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
llm: llm,
|
llm: llm,
|
||||||
bot_user: bot_user,
|
bot_user: bot_user,
|
||||||
@@ -59,86 +59,166 @@ class AiTool < ActiveRecord::Base
|
|||||||
|
|
||||||
def self.preamble
|
def self.preamble
|
||||||
<<~JS
|
<<~JS
|
||||||
/**
|
/**
|
||||||
* Tool API Quick Reference
|
* Tool API Quick Reference
|
||||||
*
|
*
|
||||||
* Entry Functions
|
* Entry Functions
|
||||||
*
|
*
|
||||||
* invoke(parameters): Main function. Receives parameters (Object). Must return a JSON-serializable value.
|
* invoke(parameters): Main function. Receives parameters defined in the tool's signature (Object).
|
||||||
* Example:
|
* Must return a JSON-serializable value (e.g., string, number, object, array).
|
||||||
* function invoke(parameters) { return "result"; }
|
* Example:
|
||||||
*
|
* function invoke(parameters) { return { result: "Data processed", input: parameters.query }; }
|
||||||
* details(): Optional. Returns a string describing the tool.
|
*
|
||||||
* Example:
|
* details(): Optional function. Returns a string (can include basic HTML) describing
|
||||||
* function details() { return "Tool description."; }
|
* the tool's action after invocation, often using data from the invocation.
|
||||||
*
|
* This is displayed in the chat interface.
|
||||||
* Provided Objects
|
* Example:
|
||||||
*
|
* let lastUrl;
|
||||||
* 1. http
|
* function invoke(parameters) {
|
||||||
* http.get(url, options?): Performs an HTTP GET request.
|
* lastUrl = parameters.url;
|
||||||
* Parameters:
|
* // ... perform action ...
|
||||||
* url (string): The request URL.
|
* return { success: true, content: "..." };
|
||||||
* options (Object, optional):
|
* }
|
||||||
* headers (Object): Request headers.
|
* function details() {
|
||||||
* Returns:
|
* return `Browsed: <a href="${lastUrl}">${lastUrl}</a>`;
|
||||||
* { status: number, body: string }
|
* }
|
||||||
*
|
*
|
||||||
* http.post(url, options?): Performs an HTTP POST request.
|
* Provided Objects & Functions
|
||||||
* Parameters:
|
*
|
||||||
* url (string): The request URL.
|
* 1. http
|
||||||
* options (Object, optional):
|
* Performs HTTP requests. Max 20 requests per execution.
|
||||||
* headers (Object): Request headers.
|
*
|
||||||
* body (string): Request body.
|
* http.get(url, options?): Performs GET request.
|
||||||
* Returns:
|
* Parameters:
|
||||||
* { status: number, body: string }
|
* url (string): The request URL.
|
||||||
*
|
* options (Object, optional):
|
||||||
* (also available: http.put, http.patch, http.delete)
|
* headers (Object): Request headers (e.g., { "Authorization": "Bearer key" }).
|
||||||
*
|
* Returns: { status: number, body: string }
|
||||||
* Note: Max 20 HTTP requests per execution.
|
*
|
||||||
*
|
* http.post(url, options?): Performs POST request.
|
||||||
* 2. llm
|
* Parameters:
|
||||||
* llm.truncate(text, length): Truncates text to a specified token length.
|
* url (string): The request URL.
|
||||||
* Parameters:
|
* options (Object, optional):
|
||||||
* text (string): Text to truncate.
|
* headers (Object): Request headers.
|
||||||
* length (number): Max tokens.
|
* body (string | Object): Request body. If an object, it's stringified as JSON.
|
||||||
* Returns:
|
* Returns: { status: number, body: string }
|
||||||
* Truncated string.
|
*
|
||||||
*
|
* http.put(url, options?): Performs PUT request (similar to POST).
|
||||||
* 3. index
|
* http.patch(url, options?): Performs PATCH request (similar to POST).
|
||||||
* index.search(query, options?): Searches indexed documents.
|
* http.delete(url, options?): Performs DELETE request (similar to GET/POST).
|
||||||
* Parameters:
|
*
|
||||||
* query (string): Search query.
|
* 2. llm
|
||||||
* options (Object, optional):
|
* Interacts with the Language Model.
|
||||||
* filenames (Array): Limit search to specific files.
|
*
|
||||||
* limit (number): Max fragments (up to 200).
|
* llm.truncate(text, length): Truncates text to a specified token length based on the configured LLM's tokenizer.
|
||||||
* Returns:
|
* Parameters:
|
||||||
* Array of { fragment: string, metadata: string }
|
* text (string): Text to truncate.
|
||||||
*
|
* length (number): Maximum number of tokens.
|
||||||
* 4. upload
|
* Returns: string (truncated text)
|
||||||
* upload.create(filename, base_64_content): Uploads a file.
|
*
|
||||||
* Parameters:
|
* llm.generate(prompt): Generates text using the configured LLM associated with the tool runner.
|
||||||
* filename (string): Name of the file.
|
* Parameters:
|
||||||
* base_64_content (string): Base64 encoded file content.
|
* prompt (string | Object): The prompt. Can be a simple string or an object
|
||||||
* Returns:
|
* like { messages: [{ type: "system", content: "..." }, { type: "user", content: "..." }] }.
|
||||||
* { id: number, short_url: string }
|
* Returns: string (generated text)
|
||||||
*
|
*
|
||||||
* 5. chain
|
* 3. index
|
||||||
* chain.setCustomRaw(raw): Sets the body of the post and exist chain.
|
* Searches attached RAG (Retrieval-Augmented Generation) documents linked to this tool.
|
||||||
* Parameters:
|
*
|
||||||
* raw (string): raw content to add to post.
|
* index.search(query, options?): Searches indexed document fragments.
|
||||||
*
|
* Parameters:
|
||||||
* Constraints
|
* query (string): The search query used for semantic search.
|
||||||
*
|
* options (Object, optional):
|
||||||
* Execution Time: ≤ 2000ms
|
* filenames (Array<string>): Filter search to fragments from specific uploaded filenames.
|
||||||
* Memory: ≤ 10MB
|
* limit (number): Maximum number of fragments to return (default: 10, max: 200).
|
||||||
* HTTP Requests: ≤ 20 per execution
|
* Returns: Array<{ fragment: string, metadata: string | null }> - Ordered by relevance.
|
||||||
* Exceeding limits will result in errors or termination.
|
*
|
||||||
*
|
* 4. upload
|
||||||
* Security
|
* Handles file uploads within Discourse.
|
||||||
*
|
*
|
||||||
* Sandboxed Environment: No access to system or global objects.
|
* upload.create(filename, base_64_content): Uploads a file created by the tool, making it available in Discourse.
|
||||||
* No File System Access: Cannot read or write files.
|
* Parameters:
|
||||||
*/
|
* filename (string): The desired name for the file (basename is used for security).
|
||||||
|
* base_64_content (string): Base64 encoded content of the file.
|
||||||
|
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
|
||||||
|
*
|
||||||
|
* 5. chain
|
||||||
|
* Controls the execution flow.
|
||||||
|
*
|
||||||
|
* chain.setCustomRaw(raw): Sets the final raw content of the bot's post and immediately
|
||||||
|
* stops the tool execution chain. Useful for tools that directly
|
||||||
|
* generate the full response content (e.g., image generation tools attaching the image markdown).
|
||||||
|
* Parameters:
|
||||||
|
* raw (string): The raw Markdown content for the post.
|
||||||
|
* Returns: void
|
||||||
|
*
|
||||||
|
* 6. discourse
|
||||||
|
* Interacts with Discourse specific features. Access is generally performed as the SystemUser.
|
||||||
|
*
|
||||||
|
* discourse.search(params): Performs a Discourse search.
|
||||||
|
* Parameters:
|
||||||
|
* params (Object): Search parameters (e.g., { search_query: "keyword", with_private: true, max_results: 10 }).
|
||||||
|
* `with_private: true` searches across all posts visible to the SystemUser. `result_style: 'detailed'` is used by default.
|
||||||
|
* Returns: Object (Discourse search results structure, includes posts, topics, users etc.)
|
||||||
|
*
|
||||||
|
* discourse.getPost(post_id): Retrieves details for a specific post.
|
||||||
|
* Parameters:
|
||||||
|
* post_id (number): The ID of the post.
|
||||||
|
* Returns: Object (Post details including `raw`, nested `topic` object with ListableTopicSerializer structure) or null if not found/accessible.
|
||||||
|
*
|
||||||
|
* discourse.getTopic(topic_id): Retrieves details for a specific topic.
|
||||||
|
* Parameters:
|
||||||
|
* topic_id (number): The ID of the topic.
|
||||||
|
* Returns: Object (Topic details using ListableTopicSerializer structure) or null if not found/accessible.
|
||||||
|
*
|
||||||
|
* discourse.getUser(user_id_or_username): Retrieves details for a specific user.
|
||||||
|
* Parameters:
|
||||||
|
* user_id_or_username (number | string): The ID or username of the user.
|
||||||
|
* Returns: Object (User details using UserSerializer structure) or null if not found.
|
||||||
|
*
|
||||||
|
* discourse.getPersona(name): Gets an object representing another AI Persona configured on the site.
|
||||||
|
* Parameters:
|
||||||
|
* name (string): The name of the target persona.
|
||||||
|
* Returns: Object { respondTo: function(params) } or null if persona not found.
|
||||||
|
* respondTo(params): Instructs the target persona to generate a response within the current context (e.g., replying to the same post or chat message).
|
||||||
|
* Parameters:
|
||||||
|
* params (Object, optional): { instructions: string, whisper: boolean }
|
||||||
|
* Returns: { success: boolean, post_id?: number, post_number?: number, message_id?: number } or { error: string }
|
||||||
|
*
|
||||||
|
* discourse.createChatMessage(params): Creates a new message in a Discourse Chat channel.
|
||||||
|
* Parameters:
|
||||||
|
* params (Object): { channel_name: string, username: string, message: string }
|
||||||
|
* `channel_name` can be the channel name or slug.
|
||||||
|
* `username` specifies the user who should appear as the sender. The user must exist.
|
||||||
|
* The sending user must have permission to post in the channel.
|
||||||
|
* Returns: { success: boolean, message_id?: number, message?: string, created_at?: string } or { error: string }
|
||||||
|
*
|
||||||
|
* 7. context
|
||||||
|
* An object containing information about the environment where the tool is being run.
|
||||||
|
* Available properties depend on the invocation context, but may include:
|
||||||
|
* post_id (number): ID of the post triggering the tool (if in a Post context).
|
||||||
|
* topic_id (number): ID of the topic (if in a Post context).
|
||||||
|
* private_message (boolean): Whether the context is a private message (in Post context).
|
||||||
|
* message_id (number): ID of the chat message triggering the tool (if in Chat context).
|
||||||
|
* channel_id (number): ID of the chat channel (if in Chat context).
|
||||||
|
* user (Object): Details of the user invoking the tool/persona (structure may vary, often null or SystemUser details unless explicitly passed).
|
||||||
|
* participants (string): Comma-separated list of usernames in a PM (if applicable).
|
||||||
|
* // ... other potential context-specific properties added by the calling environment.
|
||||||
|
*
|
||||||
|
* Constraints
|
||||||
|
*
|
||||||
|
* Execution Time: ≤ 2000ms (default timeout in milliseconds) - This timer *pauses* during external HTTP requests or LLM calls initiated via `http.*` or `llm.generate`, but applies to the script's own processing time.
|
||||||
|
* Memory: ≤ 10MB (V8 heap limit)
|
||||||
|
* Stack Depth: ≤ 20 (Marshal stack depth limit for Ruby interop)
|
||||||
|
* HTTP Requests: ≤ 20 per execution
|
||||||
|
* Exceeding limits will result in errors or termination (e.g., timeout error, out-of-memory error, TooManyRequestsError).
|
||||||
|
*
|
||||||
|
* Security
|
||||||
|
*
|
||||||
|
* Sandboxed Environment: The script runs in a restricted V8 JavaScript environment (via MiniRacer).
|
||||||
|
* No direct access to browser or environment, browser globals (like `window` or `document`), or the host system's file system.
|
||||||
|
* Network requests are proxied through the Discourse backend, not made directly from the sandbox.
|
||||||
|
*/
|
||||||
JS
|
JS
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class EmbeddingDefinition < ActiveRecord::Base
|
|||||||
DiscourseAi::Tokenizer::GeminiTokenizer,
|
DiscourseAi::Tokenizer::GeminiTokenizer,
|
||||||
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
|
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
|
||||||
DiscourseAi::Tokenizer::OpenAiTokenizer,
|
DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||||
|
DiscourseAi::Tokenizer::MixtralTokenizer,
|
||||||
].map(&:name)
|
].map(&:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ class LlmModel < ActiveRecord::Base
|
|||||||
validates :url, presence: true, unless: -> { provider == BEDROCK_PROVIDER_NAME }
|
validates :url, presence: true, unless: -> { provider == BEDROCK_PROVIDER_NAME }
|
||||||
validates_presence_of :name, :api_key
|
validates_presence_of :name, :api_key
|
||||||
validates :max_prompt_tokens, numericality: { greater_than: 0 }
|
validates :max_prompt_tokens, numericality: { greater_than: 0 }
|
||||||
|
validates :input_cost,
|
||||||
|
:cached_input_cost,
|
||||||
|
:output_cost,
|
||||||
|
:max_output_tokens,
|
||||||
|
numericality: {
|
||||||
|
greater_than_or_equal_to: 0,
|
||||||
|
},
|
||||||
|
allow_nil: true
|
||||||
validate :required_provider_params
|
validate :required_provider_params
|
||||||
scope :in_use,
|
scope :in_use,
|
||||||
-> do
|
-> do
|
||||||
@@ -55,6 +63,8 @@ class LlmModel < ActiveRecord::Base
|
|||||||
},
|
},
|
||||||
google: {
|
google: {
|
||||||
disable_native_tools: :checkbox,
|
disable_native_tools: :checkbox,
|
||||||
|
enable_thinking: :checkbox,
|
||||||
|
thinking_tokens: :number,
|
||||||
},
|
},
|
||||||
azure: {
|
azure: {
|
||||||
disable_native_tools: :checkbox,
|
disable_native_tools: :checkbox,
|
||||||
@@ -183,4 +193,8 @@ end
|
|||||||
# enabled_chat_bot :boolean default(FALSE), not null
|
# enabled_chat_bot :boolean default(FALSE), not null
|
||||||
# provider_params :jsonb
|
# provider_params :jsonb
|
||||||
# vision_enabled :boolean default(FALSE), not null
|
# vision_enabled :boolean default(FALSE), not null
|
||||||
|
# input_cost :float
|
||||||
|
# cached_input_cost :float
|
||||||
|
# output_cost :float
|
||||||
|
# max_output_tokens :integer
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiFeaturesPersonaSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :system_prompt, :allowed_groups, :enabled
|
||||||
|
|
||||||
|
def allowed_groups
|
||||||
|
Group
|
||||||
|
.where(id: object.allowed_group_ids)
|
||||||
|
.pluck(:id, :name)
|
||||||
|
.map { |id, name| { id: id, name: name } }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,7 +11,7 @@ class AiSentimentPostSerializer < ApplicationSerializer
|
|||||||
:excerpt,
|
:excerpt,
|
||||||
:sentiment,
|
:sentiment,
|
||||||
:truncated,
|
:truncated,
|
||||||
:category,
|
:category_id,
|
||||||
:created_at
|
:created_at
|
||||||
|
|
||||||
def avatar_template
|
def avatar_template
|
||||||
@@ -25,14 +25,4 @@ class AiSentimentPostSerializer < ApplicationSerializer
|
|||||||
def truncated
|
def truncated
|
||||||
object.post_cooked.length > SiteSetting.post_excerpt_maxlength
|
object.post_cooked.length > SiteSetting.post_excerpt_maxlength
|
||||||
end
|
end
|
||||||
|
|
||||||
def category
|
|
||||||
{
|
|
||||||
id: object.category_id,
|
|
||||||
name: object.category_name,
|
|
||||||
color: object.category_color,
|
|
||||||
slug: object.category_slug,
|
|
||||||
description: object.category_description,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class AiUsageSerializer < ApplicationSerializer
|
|||||||
total_cached_tokens
|
total_cached_tokens
|
||||||
total_request_tokens
|
total_request_tokens
|
||||||
total_response_tokens
|
total_response_tokens
|
||||||
|
input_spending
|
||||||
|
output_spending
|
||||||
|
cached_input_spending
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -35,6 +38,9 @@ class AiUsageSerializer < ApplicationSerializer
|
|||||||
total_cached_tokens
|
total_cached_tokens
|
||||||
total_request_tokens
|
total_request_tokens
|
||||||
total_response_tokens
|
total_response_tokens
|
||||||
|
input_spending
|
||||||
|
output_spending
|
||||||
|
cached_input_spending
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -49,6 +55,9 @@ class AiUsageSerializer < ApplicationSerializer
|
|||||||
total_cached_tokens: user.total_cached_tokens,
|
total_cached_tokens: user.total_cached_tokens,
|
||||||
total_request_tokens: user.total_request_tokens,
|
total_request_tokens: user.total_request_tokens,
|
||||||
total_response_tokens: user.total_response_tokens,
|
total_response_tokens: user.total_response_tokens,
|
||||||
|
input_spending: user.input_spending,
|
||||||
|
output_spending: user.output_spending,
|
||||||
|
cached_input_spending: user.cached_input_spending,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -60,6 +69,7 @@ class AiUsageSerializer < ApplicationSerializer
|
|||||||
total_request_tokens: object.total_request_tokens,
|
total_request_tokens: object.total_request_tokens,
|
||||||
total_response_tokens: object.total_response_tokens,
|
total_response_tokens: object.total_response_tokens,
|
||||||
total_requests: object.total_requests,
|
total_requests: object.total_requests,
|
||||||
|
total_spending: object.total_spending,
|
||||||
date_range: {
|
date_range: {
|
||||||
start: object.start_date,
|
start: object.start_date,
|
||||||
end: object.end_date,
|
end: object.end_date,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user