Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78bd1d9112 | ||
|
|
a44fc68342 | ||
|
|
10678b796e |
@@ -1,5 +1,3 @@
|
||||
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
|
||||
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
|
||||
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
|
||||
< 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87
|
||||
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
|
||||
|
||||
@@ -5,6 +5,3 @@ node_modules
|
||||
evals/log
|
||||
evals/cases
|
||||
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
|
||||
prettier_print (1.2.1)
|
||||
racc (1.8.1)
|
||||
rack (3.1.14)
|
||||
rack (3.1.12)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.74.0)
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"tests": {
|
||||
"requiredPlugins": [
|
||||
"https://github.com/discourse/discourse-prometheus",
|
||||
"https://github.com/discourse/discourse-solved.git"
|
||||
"https://github.com/discourse/discourse-prometheus"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
+1
-10
@@ -2,17 +2,8 @@ import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiLlmsEdit extends DiscourseRoute {
|
||||
async model(params) {
|
||||
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 id = parseInt(params.id, 10);
|
||||
const record = allLlms.findBy("id", id);
|
||||
record.provider_params = record.provider_params || {};
|
||||
return record;
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
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
@@ -1,156 +0,0 @@
|
||||
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>
|
||||
}
|
||||
);
|
||||
@@ -1,36 +0,0 @@
|
||||
# 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,13 +157,9 @@ module DiscourseAi
|
||||
:provider,
|
||||
:tokenizer,
|
||||
:max_prompt_tokens,
|
||||
:max_output_tokens,
|
||||
:api_key,
|
||||
:enabled_chat_bot,
|
||||
:vision_enabled,
|
||||
:input_cost,
|
||||
:cached_input_cost,
|
||||
:output_cost,
|
||||
)
|
||||
|
||||
provider = updating ? updating.provider : permitted[:provider]
|
||||
|
||||
@@ -15,7 +15,7 @@ module DiscourseAi
|
||||
LocalizedAiPersonaSerializer.new(persona, root: false)
|
||||
end
|
||||
tools =
|
||||
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
|
||||
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
|
||||
AiToolSerializer.new(tool, root: false)
|
||||
end
|
||||
AiTool
|
||||
@@ -221,14 +221,6 @@ module DiscourseAi
|
||||
permitted[:tools] = permit_tools(tools)
|
||||
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
|
||||
end
|
||||
|
||||
@@ -243,24 +235,6 @@ module DiscourseAi
|
||||
[tool, options, !!force_tool]
|
||||
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
|
||||
|
||||
@@ -55,7 +55,7 @@ module DiscourseAi
|
||||
# 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)
|
||||
runner = @ai_tool.runner(parameters, llm: llm, bot_user: current_user, context: {})
|
||||
result = runner.invoke
|
||||
|
||||
if result.is_a?(Hash) && result[:error]
|
||||
|
||||
@@ -42,21 +42,6 @@ module DiscourseAi
|
||||
<style>
|
||||
#{artifact.css}
|
||||
</style>
|
||||
<script>
|
||||
window._discourse_user_data = {
|
||||
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
|
||||
};
|
||||
window.discourseArtifactReady = new Promise(resolve => {
|
||||
window._resolveArtifactData = resolve;
|
||||
});
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'discourse-artifact-data') {
|
||||
window.discourseArtifactData = event.data.dataset || {};
|
||||
Object.assign(window.discourseArtifactData, window._discourse_user_data);
|
||||
window._resolveArtifactData(window.discourseArtifactData);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
#{artifact.html}
|
||||
@@ -89,19 +74,6 @@ module DiscourseAi
|
||||
</head>
|
||||
<body>
|
||||
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
||||
<script>
|
||||
document.querySelector('iframe').addEventListener('load', function() {
|
||||
try {
|
||||
const iframeWindow = this.contentWindow;
|
||||
const message = { type: 'discourse-artifact-data', dataset: {} };
|
||||
|
||||
if (window.frameElement && window.frameElement.dataset) {
|
||||
Object.assign(message.dataset, window.frameElement.dataset);
|
||||
}
|
||||
iframeWindow.postMessage(message, '*');
|
||||
} catch (e) { console.error('Error passing data to artifact:', e); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
@@ -47,9 +47,9 @@ module DiscourseAi
|
||||
|
||||
def discover
|
||||
ai_persona =
|
||||
AiPersona
|
||||
.all_personas(enabled_only: false)
|
||||
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
|
||||
AiPersona.all_personas.find do |persona|
|
||||
persona.id == SiteSetting.ai_bot_discover_persona.to_i
|
||||
end
|
||||
|
||||
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
|
||||
raise Discourse::InvalidAccess.new
|
||||
@@ -69,40 +69,6 @@ module DiscourseAi
|
||||
|
||||
render json: {}, status: 200
|
||||
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
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
@@ -8,7 +8,7 @@ module DiscourseAi
|
||||
before_action :require_site_settings!
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
||||
skip_before_action :redirect_to_login_if_required, :verify_authenticity_token, only: %i[asset]
|
||||
skip_before_action :verify_authenticity_token, only: ["asset"]
|
||||
|
||||
def create
|
||||
ensure_allowed_create!
|
||||
|
||||
@@ -43,7 +43,7 @@ module DiscourseAi
|
||||
prompt,
|
||||
input,
|
||||
current_user,
|
||||
force_default_locale: force_default_locale,
|
||||
force_default_locale,
|
||||
),
|
||||
status: 200
|
||||
end
|
||||
@@ -110,50 +110,26 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def stream_suggestion
|
||||
post_id = get_post_param!
|
||||
text = get_text_param!
|
||||
|
||||
location = params[:location]
|
||||
raise Discourse::InvalidParameters.new(:location) if !location
|
||||
|
||||
post = Post.includes(:topic).find_by(id: post_id)
|
||||
prompt = CompletionPrompt.find_by(id: params[:mode])
|
||||
|
||||
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
||||
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||
raise Discourse::InvalidParameters.new(:post_id) unless post
|
||||
|
||||
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
||||
end
|
||||
|
||||
# to stream we must have an appropriate client_id
|
||||
# otherwise we may end up streaming the data to the wrong client
|
||||
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
|
||||
|
||||
if location == "composer"
|
||||
Jobs.enqueue(
|
||||
:stream_composer_helper,
|
||||
user_id: current_user.id,
|
||||
text: text,
|
||||
prompt: prompt.name,
|
||||
custom_prompt: params[:custom_prompt],
|
||||
force_default_locale: params[:force_default_locale] || false,
|
||||
client_id: params[:client_id],
|
||||
)
|
||||
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],
|
||||
client_id: params[:client_id],
|
||||
)
|
||||
end
|
||||
Jobs.enqueue(
|
||||
:stream_post_helper,
|
||||
post_id: post.id,
|
||||
user_id: current_user.id,
|
||||
text: text,
|
||||
prompt: prompt.name,
|
||||
custom_prompt: params[:custom_prompt],
|
||||
)
|
||||
|
||||
render json: { success: true }, status: 200
|
||||
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed
|
||||
|
||||
@@ -46,19 +46,11 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
hijack do
|
||||
begin
|
||||
semantic_search
|
||||
.search_for_topics(query, _page = 1, hyde: !skip_hyde)
|
||||
.each { |topic_post| grouped_results.add(topic_post) }
|
||||
semantic_search
|
||||
.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,
|
||||
)
|
||||
rescue Discourse::InvalidAccess
|
||||
render_json_error(I18n.t("invalid_access"), status: 403)
|
||||
end
|
||||
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ module DiscourseAi
|
||||
u.name,
|
||||
u.uploaded_avatar_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
|
||||
WHEN (cr.classification::jsonb->'positive')::float > :threshold THEN 'positive'
|
||||
WHEN (cr.classification::jsonb->'negative')::float > :threshold THEN 'negative'
|
||||
|
||||
@@ -12,11 +12,11 @@ module ::Jobs
|
||||
return if message.blank?
|
||||
|
||||
personaClass =
|
||||
DiscourseAi::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
||||
return if personaClass.blank?
|
||||
|
||||
user = User.find_by(id: personaClass.user_id)
|
||||
bot = DiscourseAi::Personas::Bot.as(user, persona: personaClass.new)
|
||||
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
|
||||
|
||||
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
|
||||
message,
|
||||
|
||||
@@ -10,13 +10,13 @@ module ::Jobs
|
||||
persona_id = args[:persona_id]
|
||||
|
||||
begin
|
||||
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
|
||||
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
|
||||
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id)
|
||||
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
|
||||
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
|
||||
|
||||
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
|
||||
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
|
||||
rescue DiscourseAi::AiBot::Bot::BOT_NOT_FOUND
|
||||
Rails.logger.warn(
|
||||
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
||||
)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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]
|
||||
return unless args[:client_id]
|
||||
|
||||
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],
|
||||
client_id: args[:client_id],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,8 +7,6 @@ module Jobs
|
||||
def execute(args)
|
||||
interaction = args[:interaction]
|
||||
|
||||
return unless SiteSetting.ai_discord_search_enabled
|
||||
|
||||
if SiteSetting.ai_discord_search_mode == "persona"
|
||||
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
|
||||
else
|
||||
|
||||
@@ -9,9 +9,9 @@ module Jobs
|
||||
return if (query = args[:query]).blank?
|
||||
|
||||
ai_persona_klass =
|
||||
AiPersona
|
||||
.all_personas(enabled_only: false)
|
||||
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
|
||||
AiPersona.all_personas.find do |persona|
|
||||
persona.id == SiteSetting.ai_bot_discover_persona.to_i
|
||||
end
|
||||
|
||||
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
|
||||
return
|
||||
@@ -19,7 +19,7 @@ module Jobs
|
||||
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
|
||||
|
||||
bot =
|
||||
DiscourseAi::Personas::Bot.as(
|
||||
DiscourseAi::AiBot::Bot.as(
|
||||
Discourse.system_user,
|
||||
persona: ai_persona_klass.new,
|
||||
model: llm_model,
|
||||
@@ -30,13 +30,9 @@ module Jobs
|
||||
|
||||
base = { query: query, model_used: llm_model.display_name }
|
||||
|
||||
context =
|
||||
DiscourseAi::Personas::BotContext.new(
|
||||
messages: [{ type: :user, content: query }],
|
||||
skip_tool_details: true,
|
||||
)
|
||||
|
||||
bot.reply(context) do |partial|
|
||||
bot.reply(
|
||||
{ conversation_context: [{ type: :user, content: query }], skip_tool_details: true },
|
||||
) do |partial|
|
||||
streamed_reply << partial
|
||||
|
||||
# Throttle updates.
|
||||
|
||||
+16
-55
@@ -13,7 +13,6 @@ class AiPersona < ActiveRecord::Base
|
||||
validate :system_persona_unchangeable, on: :update, if: :system
|
||||
validate :chat_preconditions
|
||||
validate :allowed_seeded_model, if: :default_llm_id
|
||||
validate :well_formated_examples
|
||||
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
||||
# leaves some room for growth but sets a maximum to avoid memory issues
|
||||
# we may want to revisit this in the future
|
||||
@@ -47,18 +46,13 @@ class AiPersona < ActiveRecord::Base
|
||||
|
||||
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
|
||||
|
||||
def self.all_personas(enabled_only: true)
|
||||
def self.all_personas
|
||||
persona_cache[:value] ||= AiPersona
|
||||
.ordered
|
||||
.where(enabled: true)
|
||||
.all
|
||||
.limit(MAX_PERSONAS_PER_SITE)
|
||||
.map(&:class_instance)
|
||||
|
||||
if enabled_only
|
||||
persona_cache[:value].select { |p| p.enabled }
|
||||
else
|
||||
persona_cache[:value]
|
||||
end
|
||||
end
|
||||
|
||||
def self.persona_users(user: nil)
|
||||
@@ -182,7 +176,6 @@ class AiPersona < ActiveRecord::Base
|
||||
description
|
||||
allowed_group_ids
|
||||
tool_details
|
||||
enabled
|
||||
]
|
||||
|
||||
instance_attributes = {}
|
||||
@@ -208,14 +201,14 @@ class AiPersona < ActiveRecord::Base
|
||||
if inner_name.start_with?("custom-")
|
||||
custom_tool_id = inner_name.split("-", 2).last.to_i
|
||||
if AiTool.exists?(id: custom_tool_id, enabled: true)
|
||||
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id)
|
||||
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
|
||||
end
|
||||
else
|
||||
inner_name = inner_name.gsub("Tool", "")
|
||||
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
||||
|
||||
begin
|
||||
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize
|
||||
klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize
|
||||
options[klass] = current_options if current_options
|
||||
rescue StandardError
|
||||
end
|
||||
@@ -225,26 +218,19 @@ class AiPersona < ActiveRecord::Base
|
||||
klass
|
||||
end
|
||||
|
||||
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
|
||||
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
||||
if persona_class
|
||||
return(
|
||||
# we need a new copy so we don't leak information
|
||||
# across sites
|
||||
Class.new(persona_class) do
|
||||
# required for localization
|
||||
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
|
||||
)
|
||||
instance_attributes.each do |key, value|
|
||||
# description/name are localized
|
||||
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
|
||||
end
|
||||
persona_class.define_method(:options) { options }
|
||||
return persona_class
|
||||
end
|
||||
|
||||
ai_persona_id = self.id
|
||||
|
||||
Class.new(DiscourseAi::Personas::Persona) do
|
||||
Class.new(DiscourseAi::AiBot::Personas::Persona) do
|
||||
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
|
||||
|
||||
define_singleton_method(:to_s) do
|
||||
@@ -266,8 +252,6 @@ class AiPersona < ActiveRecord::Base
|
||||
define_method(:top_p) { @ai_persona&.top_p }
|
||||
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
||||
define_method(:uploads) { @ai_persona&.uploads }
|
||||
define_method(:response_format) { @ai_persona&.response_format }
|
||||
define_method(:examples) { @ai_persona&.examples }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -328,11 +312,9 @@ class AiPersona < ActiveRecord::Base
|
||||
end
|
||||
|
||||
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? ||
|
||||
description_changed?
|
||||
errors.add(:base, error_msg)
|
||||
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
|
||||
elsif tools_changed?
|
||||
old_tools = tools_change[0]
|
||||
new_tools = tools_change[1]
|
||||
@@ -340,17 +322,9 @@ class AiPersona < ActiveRecord::Base
|
||||
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
|
||||
|
||||
errors.add(:base, error_msg) if old_tool_names != new_tool_names
|
||||
elsif response_format_changed?
|
||||
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
|
||||
if old_tool_names != new_tool_names
|
||||
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -371,17 +345,6 @@ class AiPersona < ActiveRecord::Base
|
||||
|
||||
errors.add(:default_llm, I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"))
|
||||
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
|
||||
|
||||
# == Schema Information
|
||||
@@ -419,8 +382,6 @@ end
|
||||
# rag_llm_model_id :bigint
|
||||
# default_llm_id :bigint
|
||||
# question_consolidator_llm_id :bigint
|
||||
# response_format :jsonb
|
||||
# examples :jsonb
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
+82
-162
@@ -35,8 +35,8 @@ class AiTool < ActiveRecord::Base
|
||||
tool_name.presence || name
|
||||
end
|
||||
|
||||
def runner(parameters, llm:, bot_user:, context: nil)
|
||||
DiscourseAi::Personas::ToolRunner.new(
|
||||
def runner(parameters, llm:, bot_user:, context: {})
|
||||
DiscourseAi::AiBot::ToolRunner.new(
|
||||
parameters: parameters,
|
||||
llm: llm,
|
||||
bot_user: bot_user,
|
||||
@@ -59,166 +59,86 @@ class AiTool < ActiveRecord::Base
|
||||
|
||||
def self.preamble
|
||||
<<~JS
|
||||
/**
|
||||
* Tool API Quick Reference
|
||||
*
|
||||
* Entry Functions
|
||||
*
|
||||
* invoke(parameters): Main function. Receives parameters defined in the tool's signature (Object).
|
||||
* Must return a JSON-serializable value (e.g., string, number, object, array).
|
||||
* Example:
|
||||
* function invoke(parameters) { return { result: "Data processed", input: parameters.query }; }
|
||||
*
|
||||
* details(): Optional function. Returns a string (can include basic HTML) describing
|
||||
* the tool's action after invocation, often using data from the invocation.
|
||||
* This is displayed in the chat interface.
|
||||
* Example:
|
||||
* let lastUrl;
|
||||
* function invoke(parameters) {
|
||||
* lastUrl = parameters.url;
|
||||
* // ... perform action ...
|
||||
* return { success: true, content: "..." };
|
||||
* }
|
||||
* function details() {
|
||||
* return `Browsed: <a href="${lastUrl}">${lastUrl}</a>`;
|
||||
* }
|
||||
*
|
||||
* Provided Objects & Functions
|
||||
*
|
||||
* 1. http
|
||||
* Performs HTTP requests. Max 20 requests per execution.
|
||||
*
|
||||
* http.get(url, options?): Performs GET request.
|
||||
* Parameters:
|
||||
* url (string): The request URL.
|
||||
* options (Object, optional):
|
||||
* headers (Object): Request headers (e.g., { "Authorization": "Bearer key" }).
|
||||
* Returns: { status: number, body: string }
|
||||
*
|
||||
* http.post(url, options?): Performs POST request.
|
||||
* Parameters:
|
||||
* url (string): The request URL.
|
||||
* options (Object, optional):
|
||||
* headers (Object): Request headers.
|
||||
* body (string | Object): Request body. If an object, it's stringified as JSON.
|
||||
* Returns: { status: number, body: string }
|
||||
*
|
||||
* http.put(url, options?): Performs PUT request (similar to POST).
|
||||
* http.patch(url, options?): Performs PATCH request (similar to POST).
|
||||
* http.delete(url, options?): Performs DELETE request (similar to GET/POST).
|
||||
*
|
||||
* 2. llm
|
||||
* Interacts with the Language Model.
|
||||
*
|
||||
* llm.truncate(text, length): Truncates text to a specified token length based on the configured LLM's tokenizer.
|
||||
* Parameters:
|
||||
* text (string): Text to truncate.
|
||||
* length (number): Maximum number of tokens.
|
||||
* Returns: string (truncated text)
|
||||
*
|
||||
* llm.generate(prompt): Generates text using the configured LLM associated with the tool runner.
|
||||
* Parameters:
|
||||
* prompt (string | Object): The prompt. Can be a simple string or an object
|
||||
* like { messages: [{ type: "system", content: "..." }, { type: "user", content: "..." }] }.
|
||||
* Returns: string (generated text)
|
||||
*
|
||||
* 3. index
|
||||
* Searches attached RAG (Retrieval-Augmented Generation) documents linked to this tool.
|
||||
*
|
||||
* index.search(query, options?): Searches indexed document fragments.
|
||||
* Parameters:
|
||||
* query (string): The search query used for semantic search.
|
||||
* options (Object, optional):
|
||||
* filenames (Array<string>): Filter search to fragments from specific uploaded filenames.
|
||||
* limit (number): Maximum number of fragments to return (default: 10, max: 200).
|
||||
* Returns: Array<{ fragment: string, metadata: string | null }> - Ordered by relevance.
|
||||
*
|
||||
* 4. upload
|
||||
* Handles file uploads within Discourse.
|
||||
*
|
||||
* upload.create(filename, base_64_content): Uploads a file created by the tool, making it available in Discourse.
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Tool API Quick Reference
|
||||
*
|
||||
* Entry Functions
|
||||
*
|
||||
* invoke(parameters): Main function. Receives parameters (Object). Must return a JSON-serializable value.
|
||||
* Example:
|
||||
* function invoke(parameters) { return "result"; }
|
||||
*
|
||||
* details(): Optional. Returns a string describing the tool.
|
||||
* Example:
|
||||
* function details() { return "Tool description."; }
|
||||
*
|
||||
* Provided Objects
|
||||
*
|
||||
* 1. http
|
||||
* http.get(url, options?): Performs an HTTP GET request.
|
||||
* Parameters:
|
||||
* url (string): The request URL.
|
||||
* options (Object, optional):
|
||||
* headers (Object): Request headers.
|
||||
* Returns:
|
||||
* { status: number, body: string }
|
||||
*
|
||||
* http.post(url, options?): Performs an HTTP POST request.
|
||||
* Parameters:
|
||||
* url (string): The request URL.
|
||||
* options (Object, optional):
|
||||
* headers (Object): Request headers.
|
||||
* body (string): Request body.
|
||||
* Returns:
|
||||
* { status: number, body: string }
|
||||
*
|
||||
* (also available: http.put, http.patch, http.delete)
|
||||
*
|
||||
* Note: Max 20 HTTP requests per execution.
|
||||
*
|
||||
* 2. llm
|
||||
* llm.truncate(text, length): Truncates text to a specified token length.
|
||||
* Parameters:
|
||||
* text (string): Text to truncate.
|
||||
* length (number): Max tokens.
|
||||
* Returns:
|
||||
* Truncated string.
|
||||
*
|
||||
* 3. index
|
||||
* index.search(query, options?): Searches indexed documents.
|
||||
* Parameters:
|
||||
* query (string): Search query.
|
||||
* options (Object, optional):
|
||||
* filenames (Array): Limit search to specific files.
|
||||
* limit (number): Max fragments (up to 200).
|
||||
* Returns:
|
||||
* Array of { fragment: string, metadata: string }
|
||||
*
|
||||
* 4. upload
|
||||
* upload.create(filename, base_64_content): Uploads a file.
|
||||
* Parameters:
|
||||
* filename (string): Name of the file.
|
||||
* base_64_content (string): Base64 encoded file content.
|
||||
* Returns:
|
||||
* { id: number, short_url: string }
|
||||
*
|
||||
* 5. chain
|
||||
* chain.setCustomRaw(raw): Sets the body of the post and exist chain.
|
||||
* Parameters:
|
||||
* raw (string): raw content to add to post.
|
||||
*
|
||||
* Constraints
|
||||
*
|
||||
* Execution Time: ≤ 2000ms
|
||||
* Memory: ≤ 10MB
|
||||
* HTTP Requests: ≤ 20 per execution
|
||||
* Exceeding limits will result in errors or termination.
|
||||
*
|
||||
* Security
|
||||
*
|
||||
* Sandboxed Environment: No access to system or global objects.
|
||||
* No File System Access: Cannot read or write files.
|
||||
*/
|
||||
JS
|
||||
end
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ class EmbeddingDefinition < ActiveRecord::Base
|
||||
DiscourseAi::Tokenizer::GeminiTokenizer,
|
||||
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
|
||||
DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||
DiscourseAi::Tokenizer::MixtralTokenizer,
|
||||
].map(&:name)
|
||||
end
|
||||
|
||||
|
||||
@@ -13,14 +13,6 @@ class LlmModel < ActiveRecord::Base
|
||||
validates :url, presence: true, unless: -> { provider == BEDROCK_PROVIDER_NAME }
|
||||
validates_presence_of :name, :api_key
|
||||
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
|
||||
scope :in_use,
|
||||
-> do
|
||||
@@ -63,8 +55,6 @@ class LlmModel < ActiveRecord::Base
|
||||
},
|
||||
google: {
|
||||
disable_native_tools: :checkbox,
|
||||
enable_thinking: :checkbox,
|
||||
thinking_tokens: :number,
|
||||
},
|
||||
azure: {
|
||||
disable_native_tools: :checkbox,
|
||||
@@ -193,8 +183,4 @@ end
|
||||
# enabled_chat_bot :boolean default(FALSE), not null
|
||||
# provider_params :jsonb
|
||||
# vision_enabled :boolean default(FALSE), not null
|
||||
# input_cost :float
|
||||
# cached_input_cost :float
|
||||
# output_cost :float
|
||||
# max_output_tokens :integer
|
||||
#
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user