Merge branch 'main' into ref-locale-arg
This commit is contained in:
commit
c43d5a1801
|
@ -25,25 +25,24 @@ module DiscourseAi
|
||||||
input = get_text_param!
|
input = get_text_param!
|
||||||
force_default_locale = params[:force_default_locale] || false
|
force_default_locale = params[:force_default_locale] || false
|
||||||
|
|
||||||
prompt = CompletionPrompt.find_by(id: params[:mode])
|
raise Discourse::InvalidParameters.new(:mode) if params[:mode].blank?
|
||||||
|
|
||||||
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
if params[:mode] == DiscourseAi::AiHelper::Assistant::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?
|
||||||
|
|
||||||
prompt.custom_instruction = params[:custom_prompt]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
if params[:mode] == DiscourseAi::AiHelper::Assistant::ILLUSTRATE_POST
|
||||||
|
return suggest_thumbnails(input)
|
||||||
|
end
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
render json:
|
render json:
|
||||||
DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(
|
DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(
|
||||||
prompt,
|
params[:mode],
|
||||||
input,
|
input,
|
||||||
current_user,
|
current_user,
|
||||||
force_default_locale: force_default_locale,
|
force_default_locale: force_default_locale,
|
||||||
|
custom_prompt: params[:custom_prompt],
|
||||||
),
|
),
|
||||||
status: 200
|
status: 200
|
||||||
end
|
end
|
||||||
|
@ -60,13 +59,10 @@ module DiscourseAi
|
||||||
input = get_text_param!
|
input = get_text_param!
|
||||||
end
|
end
|
||||||
|
|
||||||
prompt = CompletionPrompt.enabled_by_name("generate_titles")
|
|
||||||
raise Discourse::InvalidParameters.new(:mode) if !prompt
|
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
render json:
|
render json:
|
||||||
DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(
|
DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(
|
||||||
prompt,
|
DiscourseAi::AiHelper::Assistant::GENERATE_TITLES,
|
||||||
input,
|
input,
|
||||||
current_user,
|
current_user,
|
||||||
),
|
),
|
||||||
|
@ -115,12 +111,12 @@ module DiscourseAi
|
||||||
location = params[:location]
|
location = params[:location]
|
||||||
raise Discourse::InvalidParameters.new(:location) if !location
|
raise Discourse::InvalidParameters.new(:location) if !location
|
||||||
|
|
||||||
prompt = CompletionPrompt.find_by(id: params[:mode])
|
raise Discourse::InvalidParameters.new(:mode) if params[:mode].blank?
|
||||||
|
if params[:mode] == DiscourseAi::AiHelper::Assistant::ILLUSTRATE_POST
|
||||||
|
return suggest_thumbnails(input)
|
||||||
|
end
|
||||||
|
|
||||||
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
if params[:mode] == DiscourseAi::AiHelper::Assistant::CUSTOM_PROMPT
|
||||||
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -133,7 +129,7 @@ module DiscourseAi
|
||||||
:stream_composer_helper,
|
:stream_composer_helper,
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
text: text,
|
text: text,
|
||||||
prompt: prompt.name,
|
prompt: params[:mode],
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
force_default_locale: params[:force_default_locale] || false,
|
force_default_locale: params[:force_default_locale] || false,
|
||||||
client_id: params[:client_id],
|
client_id: params[:client_id],
|
||||||
|
@ -149,7 +145,7 @@ module DiscourseAi
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
text: text,
|
text: text,
|
||||||
prompt: prompt.name,
|
prompt: params[:mode],
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
client_id: params[:client_id],
|
client_id: params[:client_id],
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,19 +10,16 @@ module Jobs
|
||||||
return unless args[:text]
|
return unless args[:text]
|
||||||
return unless args[:client_id]
|
return unless args[:client_id]
|
||||||
|
|
||||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
helper_mode = args[:prompt]
|
||||||
|
|
||||||
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
|
||||||
prompt.custom_instruction = args[:custom_prompt]
|
|
||||||
end
|
|
||||||
|
|
||||||
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
||||||
prompt,
|
helper_mode,
|
||||||
args[:text],
|
args[:text],
|
||||||
user,
|
user,
|
||||||
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||||
force_default_locale: args[:force_default_locale],
|
force_default_locale: args[:force_default_locale],
|
||||||
client_id: args[:client_id],
|
client_id: args[:client_id],
|
||||||
|
custom_prompt: args[:custom_prompt],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,16 +14,12 @@ module Jobs
|
||||||
|
|
||||||
return unless user.guardian.can_see?(post)
|
return unless user.guardian.can_see?(post)
|
||||||
|
|
||||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
helper_mode = args[:prompt]
|
||||||
|
|
||||||
if prompt.id == CompletionPrompt::CUSTOM_PROMPT
|
if helper_mode == DiscourseAi::AiHelper::Assistant::EXPLAIN
|
||||||
prompt.custom_instruction = args[:custom_prompt]
|
input = <<~TEXT.strip
|
||||||
end
|
<term>#{args[:text]}</term>
|
||||||
|
<context>#{post.raw}</context>
|
||||||
if prompt.name == "explain"
|
|
||||||
input = <<~TEXT
|
|
||||||
<term>#{args[:text]}</term>
|
|
||||||
<context>#{post.raw}</context>
|
|
||||||
<topic>#{topic.title}</topic>
|
<topic>#{topic.title}</topic>
|
||||||
#{reply_to ? "<replyTo>#{reply_to.raw}</replyTo>" : nil}
|
#{reply_to ? "<replyTo>#{reply_to.raw}</replyTo>" : nil}
|
||||||
TEXT
|
TEXT
|
||||||
|
@ -32,10 +28,11 @@ module Jobs
|
||||||
end
|
end
|
||||||
|
|
||||||
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
||||||
prompt,
|
helper_mode,
|
||||||
input,
|
input,
|
||||||
user,
|
user,
|
||||||
"/discourse-ai/ai-helper/stream_suggestion/#{post.id}",
|
"/discourse-ai/ai-helper/stream_suggestion/#{post.id}",
|
||||||
|
custom_prompt: args[:custom_prompt],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ module Jobs
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
limit = SiteSetting.ai_translation_backfill_rate
|
limit = SiteSetting.ai_translation_backfill_rate
|
||||||
|
|
||||||
return if limit == 0
|
return if limit == 0
|
||||||
|
|
||||||
topics = Topic.where(locale: nil, deleted_at: nil).where("topics.user_id > 0")
|
topics = Topic.where(locale: nil, deleted_at: nil).where("topics.user_id > 0")
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CompletionPrompt < ActiveRecord::Base
|
class CompletionPrompt < ActiveRecord::Base
|
||||||
|
#
|
||||||
|
# DEPRECATED.
|
||||||
|
# TODO(roman): Remove after 06-17-25
|
||||||
|
#
|
||||||
|
|
||||||
TRANSLATE = -301
|
TRANSLATE = -301
|
||||||
GENERATE_TITLES = -307
|
GENERATE_TITLES = -307
|
||||||
PROOFREAD = -303
|
PROOFREAD = -303
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import bodyClass from "discourse/helpers/body-class";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import userAutocomplete from "discourse/lib/autocomplete/user";
|
import userAutocomplete from "discourse/lib/autocomplete/user";
|
||||||
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
||||||
|
@ -279,6 +280,7 @@ export default class AiBotConversations extends Component {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ai-bot-conversations">
|
<div class="ai-bot-conversations">
|
||||||
|
{{bodyClass "ai-bot-conversations-page"}}
|
||||||
<AiPersonaLlmSelector
|
<AiPersonaLlmSelector
|
||||||
@showLabels={{true}}
|
@showLabels={{true}}
|
||||||
@setPersonaId={{this.setPersonaId}}
|
@setPersonaId={{this.setPersonaId}}
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default class AiComposerHelperMenu extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.forEach((p) => {
|
prompts.forEach((p) => {
|
||||||
this.prompts[p.id] = p;
|
this.prompts[p.name] = p;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.promptTypes = prompts.reduce((memo, p) => {
|
this.promptTypes = prompts.reduce((memo, p) => {
|
||||||
|
@ -116,7 +116,7 @@ export default class AiComposerHelperMenu extends Component {
|
||||||
if (option.name === "illustrate_post") {
|
if (option.name === "illustrate_post") {
|
||||||
return this.modal.show(ThumbnailSuggestion, {
|
return this.modal.show(ThumbnailSuggestion, {
|
||||||
model: {
|
model: {
|
||||||
mode: option.id,
|
mode: option.name,
|
||||||
selectedText: this.args.data.selectedText,
|
selectedText: this.args.data.selectedText,
|
||||||
thumbnails: this.thumbnailSuggestions,
|
thumbnails: this.thumbnailSuggestions,
|
||||||
},
|
},
|
||||||
|
@ -128,7 +128,7 @@ export default class AiComposerHelperMenu extends Component {
|
||||||
|
|
||||||
return this.modal.show(ModalDiffModal, {
|
return this.modal.show(ModalDiffModal, {
|
||||||
model: {
|
model: {
|
||||||
mode: option.id,
|
mode: option.name,
|
||||||
selectedText: this.args.data.selectedText,
|
selectedText: this.args.data.selectedText,
|
||||||
revert: this.undoAiAction,
|
revert: this.undoAiAction,
|
||||||
toolbarEvent: this.args.data.toolbarEvent,
|
toolbarEvent: this.args.data.toolbarEvent,
|
||||||
|
|
|
@ -29,13 +29,12 @@ export default class AiHelperOptionsList extends Component {
|
||||||
@submit={{@performAction}}
|
@submit={{@performAction}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li data-name={{option.translated_name}} data-value={{option.id}}>
|
<li data-name={{option.translated_name}} data-value={{option.name}}>
|
||||||
<DButton
|
<DButton
|
||||||
@icon={{option.icon}}
|
@icon={{option.icon}}
|
||||||
@translatedLabel={{option.translated_name}}
|
@translatedLabel={{option.translated_name}}
|
||||||
@action={{fn @performAction option}}
|
@action={{fn @performAction option}}
|
||||||
data-name={{option.name}}
|
data-name={{option.name}}
|
||||||
data-value={{option.id}}
|
|
||||||
class="ai-helper-options__button"
|
class="ai-helper-options__button"
|
||||||
>
|
>
|
||||||
{{#if (and (eq option.name "proofread") this.showShortcut)}}
|
{{#if (and (eq option.name "proofread") this.showShortcut)}}
|
||||||
|
|
|
@ -530,6 +530,7 @@ export default class PersonaEditor extends Component {
|
||||||
<form.Field
|
<form.Field
|
||||||
@name="rag_uploads"
|
@name="rag_uploads"
|
||||||
@title={{i18n "discourse_ai.rag.uploads.title"}}
|
@title={{i18n "discourse_ai.rag.uploads.title"}}
|
||||||
|
@format="full"
|
||||||
as |field|
|
as |field|
|
||||||
>
|
>
|
||||||
<field.Custom>
|
<field.Custom>
|
||||||
|
|
|
@ -33,6 +33,19 @@ export default class AiPersonaToolOptions extends Component {
|
||||||
return toolOptions ? Object.keys(toolOptions) : [];
|
return toolOptions ? Object.keys(toolOptions) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toolOptionKeys(toolId) {
|
||||||
|
// this is important, some tools may not have all options defined (for example if a tool option is added)
|
||||||
|
const metadata = this.toolsMetadata[toolId];
|
||||||
|
if (!metadata) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// a bit more verbose for clarity of our selection
|
||||||
|
const availableOptions = Object.keys(metadata).filter((k) => k !== "name");
|
||||||
|
return availableOptions;
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.showToolOptions}}
|
{{#if this.showToolOptions}}
|
||||||
<@form.Container
|
<@form.Container
|
||||||
|
@ -51,8 +64,8 @@ export default class AiPersonaToolOptions extends Component {
|
||||||
<div class="ai-persona-editor__tool-options-name">
|
<div class="ai-persona-editor__tool-options-name">
|
||||||
{{toolMeta.name}}
|
{{toolMeta.name}}
|
||||||
</div>
|
</div>
|
||||||
<toolObj.Object @name={{toolId}} as |optionsObj optionData|>
|
<toolObj.Object @name={{toolId}} as |optionsObj|>
|
||||||
{{#each (this.formObjectKeys optionData) as |optionName|}}
|
{{#each (this.toolOptionKeys toolId) as |optionName|}}
|
||||||
{{#let (get toolMeta optionName) as |optionMeta|}}
|
{{#let (get toolMeta optionName) as |optionMeta|}}
|
||||||
<optionsObj.Field
|
<optionsObj.Field
|
||||||
@name={{optionName}}
|
@name={{optionName}}
|
||||||
|
|
|
@ -202,7 +202,7 @@ export default class AiPostHelperMenu extends Component {
|
||||||
this._activeAiRequest = ajax("/discourse-ai/ai-helper/suggest", {
|
this._activeAiRequest = ajax("/discourse-ai/ai-helper/suggest", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
mode: option.id,
|
mode: option.name,
|
||||||
text: this.args.data.quoteState.buffer,
|
text: this.args.data.quoteState.buffer,
|
||||||
custom_prompt: this.customPromptValue,
|
custom_prompt: this.customPromptValue,
|
||||||
},
|
},
|
||||||
|
@ -238,7 +238,7 @@ export default class AiPostHelperMenu extends Component {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
location: "post",
|
location: "post",
|
||||||
mode: option.id,
|
mode: option.name,
|
||||||
text: this.args.data.selectedText,
|
text: this.args.data.selectedText,
|
||||||
post_id: this.args.data.quoteState.postId,
|
post_id: this.args.data.quoteState.postId,
|
||||||
custom_prompt: this.customPromptValue,
|
custom_prompt: this.customPromptValue,
|
||||||
|
|
|
@ -359,6 +359,7 @@ export default class AiToolEditorForm extends Component {
|
||||||
@name="rag_uploads"
|
@name="rag_uploads"
|
||||||
@title={{i18n "discourse_ai.rag.uploads.title"}}
|
@title={{i18n "discourse_ai.rag.uploads.title"}}
|
||||||
@tooltip={{this.ragUploadsDescription}}
|
@tooltip={{this.ragUploadsDescription}}
|
||||||
|
@format="full"
|
||||||
as |field|
|
as |field|
|
||||||
>
|
>
|
||||||
<field.Custom>
|
<field.Custom>
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default class AiEditSuggestionButton extends Component {
|
||||||
this._activeAIRequest = ajax("/discourse-ai/ai-helper/suggest", {
|
this._activeAIRequest = ajax("/discourse-ai/ai-helper/suggest", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
mode: this.mode.id,
|
mode: this.mode.name,
|
||||||
text: this.args.outletArgs.initialValue,
|
text: this.args.outletArgs.initialValue,
|
||||||
custom_prompt: "",
|
custom_prompt: "",
|
||||||
},
|
},
|
||||||
|
|
|
@ -44,7 +44,7 @@ function initializeAiHelperTrigger(api) {
|
||||||
|
|
||||||
const mode = currentUser?.ai_helper_prompts.find(
|
const mode = currentUser?.ai_helper_prompts.find(
|
||||||
(p) => p.name === "proofread"
|
(p) => p.name === "proofread"
|
||||||
).id;
|
).name;
|
||||||
|
|
||||||
modal.show(ModalDiffModal, {
|
modal.show(ModalDiffModal, {
|
||||||
model: {
|
model: {
|
||||||
|
|
|
@ -78,8 +78,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.rag-uploader {
|
.rag-uploader {
|
||||||
width: 500px;
|
|
||||||
|
|
||||||
&__search-input {
|
&__search-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@use "lib/viewport";
|
||||||
|
|
||||||
.ai-tool-parameter {
|
.ai-tool-parameter {
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
border: 1px solid var(--primary-low-mid);
|
border: 1px solid var(--primary-low-mid);
|
||||||
|
@ -27,7 +29,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-tool-editor {
|
.ai-tool-editor {
|
||||||
max-width: 80%;
|
@include viewport.from(lg) {
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
#control-rag_uploads .rag-uploader {
|
#control-rag_uploads .rag-uploader {
|
||||||
|
|
|
@ -339,6 +339,34 @@ en:
|
||||||
concept_deduplicator:
|
concept_deduplicator:
|
||||||
name: "Concept Deduplicator"
|
name: "Concept Deduplicator"
|
||||||
description: "AI Bot specialized in deduplicating concepts"
|
description: "AI Bot specialized in deduplicating concepts"
|
||||||
|
custom_prompt:
|
||||||
|
name: "Custom prompt"
|
||||||
|
description: "Default persona powering the Helper's custom prompt feature"
|
||||||
|
smart_dates:
|
||||||
|
name: "Smart dates"
|
||||||
|
description: "Default persona powering the Helper's smart dates feature"
|
||||||
|
markdown_table_generator:
|
||||||
|
name: "Markdown table generator"
|
||||||
|
description: "Default persona powering the Helper's generate Markdown table feature"
|
||||||
|
post_illustrator:
|
||||||
|
name: "Post illustrator"
|
||||||
|
description: "Generates StableDiffusion prompts to power the Helper's illustrate post feature"
|
||||||
|
proofreader:
|
||||||
|
name: "Proofreader"
|
||||||
|
description: "Default persona powering the Helper's proofread text feature"
|
||||||
|
titles_generator:
|
||||||
|
name: "Titles generator"
|
||||||
|
description: "Default persona powering the Helper's suggest topic titles feature"
|
||||||
|
tutor:
|
||||||
|
name: "Tutor"
|
||||||
|
description: "Default persona powering the Helper's explain feature"
|
||||||
|
translator:
|
||||||
|
name: "Translator"
|
||||||
|
description: "Default persona powering the Helper's translator feature"
|
||||||
|
image_captioner:
|
||||||
|
name: "Image captions"
|
||||||
|
description: "Default persona powering the Helper's image caption feature"
|
||||||
|
|
||||||
topic_not_found: "Summary unavailable, topic not found!"
|
topic_not_found: "Summary unavailable, topic not found!"
|
||||||
summarizing: "Summarizing topic"
|
summarizing: "Summarizing topic"
|
||||||
searching: "Searching for: '%{query}'"
|
searching: "Searching for: '%{query}'"
|
||||||
|
@ -359,9 +387,6 @@ en:
|
||||||
include_private:
|
include_private:
|
||||||
name: "Include private"
|
name: "Include private"
|
||||||
description: "Include private topics in the filters"
|
description: "Include private topics in the filters"
|
||||||
max_tokens_per_post:
|
|
||||||
name: "Maximum tokens per post"
|
|
||||||
description: "Maximum number of tokens to use for each post in the filter"
|
|
||||||
create_artifact:
|
create_artifact:
|
||||||
creator_llm:
|
creator_llm:
|
||||||
name: "LLM"
|
name: "LLM"
|
||||||
|
|
|
@ -104,13 +104,14 @@ discourse_ai:
|
||||||
allow_any: false
|
allow_any: false
|
||||||
type: enum
|
type: enum
|
||||||
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
||||||
validator: "DiscourseAi::Configuration::LlmValidator"
|
hidden: true
|
||||||
ai_helper_custom_prompts_allowed_groups:
|
ai_helper_custom_prompts_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
|
||||||
type: group_list
|
type: group_list
|
||||||
list_type: compact
|
list_type: compact
|
||||||
default: "3" # 3: @staff
|
default: "3" # 3: @staff
|
||||||
allow_any: false
|
allow_any: false
|
||||||
refresh: true
|
refresh: true
|
||||||
|
hidden: true
|
||||||
post_ai_helper_allowed_groups:
|
post_ai_helper_allowed_groups:
|
||||||
type: group_list
|
type: group_list
|
||||||
list_type: compact
|
list_type: compact
|
||||||
|
@ -143,6 +144,7 @@ discourse_ai:
|
||||||
default: ""
|
default: ""
|
||||||
type: enum
|
type: enum
|
||||||
enum: "DiscourseAi::Configuration::LlmVisionEnumerator"
|
enum: "DiscourseAi::Configuration::LlmVisionEnumerator"
|
||||||
|
hidden: true
|
||||||
ai_auto_image_caption_allowed_groups:
|
ai_auto_image_caption_allowed_groups:
|
||||||
client: true
|
client: true
|
||||||
type: group_list
|
type: group_list
|
||||||
|
@ -160,6 +162,42 @@ discourse_ai:
|
||||||
hidden: true
|
hidden: true
|
||||||
type: list
|
type: list
|
||||||
list_type: compact
|
list_type: compact
|
||||||
|
ai_helper_proofreader_persona:
|
||||||
|
default: "-22"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_title_suggestions_persona:
|
||||||
|
default: "-23"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_explain_persona:
|
||||||
|
default: "-24"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_post_illustrator_persona:
|
||||||
|
default: "-21"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_smart_dates_persona:
|
||||||
|
default: "-19"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_translator_persona:
|
||||||
|
default: "-25"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_markdown_tables_persona:
|
||||||
|
default: "-20"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_custom_prompt_persona:
|
||||||
|
default: "-18"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
ai_helper_image_caption_persona:
|
||||||
|
default: "-26"
|
||||||
|
type: enum
|
||||||
|
enum: "DiscourseAi::Configuration::PersonaEnumerator"
|
||||||
|
|
||||||
ai_embeddings_enabled:
|
ai_embeddings_enabled:
|
||||||
default: false
|
default: false
|
||||||
|
|
|
@ -1,269 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -301
|
|
||||||
cp.name = "translate"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:text]
|
|
||||||
cp.stop_sequences = ["\n</output>", "</output>"]
|
|
||||||
cp.temperature = 0.2
|
|
||||||
cp.messages = {
|
|
||||||
insts: <<~TEXT,
|
|
||||||
I want you to act as an %LANGUAGE% translator, spelling corrector and improver. I will write to you
|
|
||||||
in any language and you will detect the language, translate it and answer in the corrected and
|
|
||||||
improved version of my text, in %LANGUAGE%. I want you to replace my simplified A0-level words and
|
|
||||||
sentences with more beautiful and elegant, upper level %LANGUAGE% words and sentences.
|
|
||||||
Keep the meaning same, but make them more literary. I want you to only reply the correction,
|
|
||||||
the improvements and nothing else, do not write explanations.
|
|
||||||
You will find the text between <input></input> XML tags.
|
|
||||||
Include your translation between <output></output> XML tags.
|
|
||||||
TEXT
|
|
||||||
examples: [["<input>Hello</input>", "<output>...%LANGUAGE% translation...</output>"]],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -303
|
|
||||||
cp.name = "proofread"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:diff]
|
|
||||||
cp.temperature = 0
|
|
||||||
cp.stop_sequences = ["\n</output>"]
|
|
||||||
cp.messages = {
|
|
||||||
insts: <<~TEXT,
|
|
||||||
You are a markdown proofreader. You correct egregious typos and phrasing issues but keep the user's original voice.
|
|
||||||
You do not touch code blocks. I will provide you with text to proofread. If nothing needs fixing, then you will echo the text back.
|
|
||||||
You will find the text between <input></input> XML tags.
|
|
||||||
You will ALWAYS return the corrected text between <output></output> XML tags.
|
|
||||||
TEXT
|
|
||||||
examples: [
|
|
||||||
[
|
|
||||||
"<input></input>",
|
|
||||||
"<output></output>",
|
|
||||||
],
|
|
||||||
[<<~TEXT, "The rain in Spain, stays mainly in the Plane."],
|
|
||||||
<input>
|
|
||||||
The rain in spain stays mainly in the plane.
|
|
||||||
</input>
|
|
||||||
TEXT
|
|
||||||
[
|
|
||||||
"<input>The rain in Spain, stays mainly in the Plane.</input>",
|
|
||||||
"<output>The rain in Spain, stays mainly in the Plane.</output>",
|
|
||||||
],
|
|
||||||
[<<~TEXT, <<~TEXT],
|
|
||||||
<input>
|
|
||||||
Hello,
|
|
||||||
|
|
||||||
Sometimes the logo isn't changing automatically when color scheme changes.
|
|
||||||
|
|
||||||

|
|
||||||
</input>
|
|
||||||
TEXT
|
|
||||||
<output>
|
|
||||||
Hello,
|
|
||||||
Sometimes the logo does not change automatically when the color scheme changes.
|
|
||||||

|
|
||||||
</output>
|
|
||||||
TEXT
|
|
||||||
[<<~TEXT, <<~TEXT],
|
|
||||||
<input>
|
|
||||||
Any ideas what is wrong with this peace of cod?
|
|
||||||
> This quot contains a typo
|
|
||||||
```ruby
|
|
||||||
# this has speling mistakes
|
|
||||||
testin.atypo = 11
|
|
||||||
baad = "bad"
|
|
||||||
```
|
|
||||||
</input>
|
|
||||||
TEXT
|
|
||||||
<output>
|
|
||||||
Any ideas what is wrong with this piece of code?
|
|
||||||
> This quot contains a typo
|
|
||||||
```ruby
|
|
||||||
# This has spelling mistakes
|
|
||||||
testing.a_typo = 11
|
|
||||||
bad = "bad"
|
|
||||||
```
|
|
||||||
</output>
|
|
||||||
TEXT
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -304
|
|
||||||
cp.name = "markdown_table"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:diff]
|
|
||||||
cp.temperature = 0.5
|
|
||||||
cp.stop_sequences = ["\n</output>"]
|
|
||||||
cp.messages = {
|
|
||||||
insts: <<~TEXT,
|
|
||||||
You are a markdown table formatter, I will provide you text inside <input></input> XML tags and you will format it into a markdown table
|
|
||||||
TEXT
|
|
||||||
examples: [
|
|
||||||
["<input>sam,joe,jane\nage: 22| 10|11</input>", <<~TEXT],
|
|
||||||
<output>
|
|
||||||
| | sam | joe | jane |
|
|
||||||
|---|---|---|---|
|
|
||||||
| age | 22 | 10 | 11 |
|
|
||||||
</output>
|
|
||||||
TEXT
|
|
||||||
[<<~TEXT, <<~TEXT],
|
|
||||||
<input>
|
|
||||||
sam: speed 100, age 22
|
|
||||||
jane: age 10
|
|
||||||
fred: height 22
|
|
||||||
</input>
|
|
||||||
TEXT
|
|
||||||
<output>
|
|
||||||
| | speed | age | height |
|
|
||||||
|---|---|---|---|
|
|
||||||
| sam | 100 | 22 | - |
|
|
||||||
| jane | - | 10 | - |
|
|
||||||
| fred | - | - | 22 |
|
|
||||||
</output>
|
|
||||||
TEXT
|
|
||||||
[<<~TEXT, <<~TEXT],
|
|
||||||
<input>
|
|
||||||
chrome 22ms (first load 10ms)
|
|
||||||
firefox 10ms (first load: 9ms)
|
|
||||||
</input>
|
|
||||||
TEXT
|
|
||||||
<output>
|
|
||||||
| Browser | Load Time (ms) | First Load Time (ms) |
|
|
||||||
|---|---|---|
|
|
||||||
| Chrome | 22 | 10 |
|
|
||||||
| Firefox | 10 | 9 |
|
|
||||||
</output>
|
|
||||||
TEXT
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -305
|
|
||||||
cp.name = "custom_prompt"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:diff]
|
|
||||||
cp.messages = { insts: <<~TEXT }
|
|
||||||
You are a helpful assistant. I will give you instructions inside <input></input> XML tags.
|
|
||||||
You will look at them and reply with a result.
|
|
||||||
TEXT
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -306
|
|
||||||
cp.name = "explain"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:text]
|
|
||||||
cp.messages = { insts: <<~TEXT }
|
|
||||||
You are a tutor explaining a term to a student in a specific context.
|
|
||||||
|
|
||||||
I will provide everything you need to know inside <input> tags, which consists of the term I want you
|
|
||||||
to explain inside <term> tags, the context of where it was used inside <context> tags, the title of
|
|
||||||
the topic where it was used inside <topic> tags, and optionally, the previous post in the conversation
|
|
||||||
in <replyTo> tags.
|
|
||||||
|
|
||||||
Using all this information, write a paragraph with a brief explanation
|
|
||||||
of what the term means. Format the response using Markdown. Reply only with the explanation and
|
|
||||||
nothing more.
|
|
||||||
TEXT
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -307
|
|
||||||
cp.name = "generate_titles"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:list]
|
|
||||||
cp.messages = {
|
|
||||||
insts: <<~TEXT,
|
|
||||||
I want you to act as a title generator for written pieces. I will provide you with a text,
|
|
||||||
and you will generate five titles. Please keep the title concise and under 20 words,
|
|
||||||
and ensure that the meaning is maintained. Replies will utilize the language type of the topic.
|
|
||||||
I want you to only reply the list of options and nothing else, do not write explanations.
|
|
||||||
Never ever use colons in the title. Always use sentence case, using a capital letter at
|
|
||||||
the start of the title, never start the title with a lower case letter. Proper nouns in the title
|
|
||||||
can have a capital letter, and acronyms like LLM can use capital letters. Format some titles
|
|
||||||
as questions, some as statements. Make sure to use question marks if the title is a question.
|
|
||||||
Each title you generate must be separated by *
|
|
||||||
You will find the text between <input></input> XML tags.
|
|
||||||
TEXT
|
|
||||||
examples: [
|
|
||||||
[
|
|
||||||
"<input>In the labyrinth of time, a solitary horse, etched in gold by the setting sun, embarked on an infinite journey.</input>",
|
|
||||||
"<item>The solitary horse</item><item>The horse etched in gold</item><item>A horse's infinite journey</item><item>A horse lost in time</item><item>A horse's last ride</item>",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
post_insts: "Wrap each title between <item></item> XML tags.",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -308
|
|
||||||
cp.name = "illustrate_post"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:list]
|
|
||||||
cp.messages = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -309
|
|
||||||
cp.name = "detect_text_locale"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:text]
|
|
||||||
cp.messages = {
|
|
||||||
insts: <<~TEXT,
|
|
||||||
I want you to act as a language expert, determining the locale for a set of text.
|
|
||||||
The locale is a language identifier, such as "en" for English, "de" for German, etc,
|
|
||||||
and can also include a region identifier, such as "en-GB" for British English, or "zh-Hans" for Simplified Chinese.
|
|
||||||
I will provide you with text, and you will determine the locale of the text.
|
|
||||||
You will find the text between <input></input> XML tags.
|
|
||||||
Include your locale between <output></output> XML tags.
|
|
||||||
TEXT
|
|
||||||
examples: [["<input>Hello my favourite colour is red</input>", "<output>en-GB</output>"]],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
CompletionPrompt.seed do |cp|
|
|
||||||
cp.id = -310
|
|
||||||
cp.name = "replace_dates"
|
|
||||||
cp.prompt_type = CompletionPrompt.prompt_types[:diff]
|
|
||||||
cp.temperature = 0
|
|
||||||
cp.stop_sequences = ["\n</output>"]
|
|
||||||
cp.messages = {
|
|
||||||
insts: <<~TEXT,
|
|
||||||
You are a date and time formatter for Discourse posts. Convert natural language time references into date placeholders.
|
|
||||||
Do not modify any markdown, code blocks, or existing date formats.
|
|
||||||
|
|
||||||
Here's the temporal context:
|
|
||||||
{{temporal_context}}
|
|
||||||
|
|
||||||
Available date placeholder formats:
|
|
||||||
- Simple day without time: {{date:1}} for tomorrow, {{date:7}} for a week from today
|
|
||||||
- Specific time: {{datetime:2pm+1}} for 2 PM tomorrow
|
|
||||||
- Time range: {{datetime:2pm+1:4pm+1}} for tomorrow 2 PM to 4 PM
|
|
||||||
|
|
||||||
You will find the text between <input></input> XML tags.
|
|
||||||
Return the text with dates converted between <output></output> XML tags.
|
|
||||||
TEXT
|
|
||||||
examples: [
|
|
||||||
[
|
|
||||||
"<input>The meeting is at 2pm tomorrow</input>",
|
|
||||||
"<output>The meeting is at {{datetime:2pm+1}}</output>",
|
|
||||||
],
|
|
||||||
["<input>Due in 3 days</input>", "<output>Due {{date:3}}</output>"],
|
|
||||||
[
|
|
||||||
"<input>Meeting next Tuesday at 2pm</input>",
|
|
||||||
"<output>Meeting {{next_week:tuesday-2pm}}</output>",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"<input>Meeting from 2pm to 4pm tomorrow</input>",
|
|
||||||
"<output>Meeting {{datetime:2pm+1:4pm+1}}</output>",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"<input>Meeting notes for tomorrow:
|
|
||||||
* Action items in `config.rb`
|
|
||||||
* Review PR #1234
|
|
||||||
* Deadline is 5pm
|
|
||||||
* Check [this link](https://example.com)</input>",
|
|
||||||
"<output>Meeting notes for {{date:1}}:
|
|
||||||
* Action items in `config.rb`
|
|
||||||
* Review PR #1234
|
|
||||||
* Deadline is {{datetime:5pm+1}}
|
|
||||||
* Check [this link](https://example.com)</output>",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
end
|
|
|
@ -3,10 +3,13 @@
|
||||||
summarization_personas = [DiscourseAi::Personas::Summarizer, DiscourseAi::Personas::ShortSummarizer]
|
summarization_personas = [DiscourseAi::Personas::Summarizer, DiscourseAi::Personas::ShortSummarizer]
|
||||||
|
|
||||||
def from_setting(setting_name)
|
def from_setting(setting_name)
|
||||||
DB.query_single(
|
DB
|
||||||
"SELECT value FROM site_settings WHERE name = :setting_name",
|
.query_single(
|
||||||
setting_name: setting_name,
|
"SELECT value FROM site_settings WHERE name = :setting_name",
|
||||||
)
|
setting_name: setting_name,
|
||||||
|
)
|
||||||
|
&.first
|
||||||
|
&.split("|")
|
||||||
end
|
end
|
||||||
|
|
||||||
DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
|
DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
|
||||||
|
@ -28,7 +31,11 @@ DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
|
||||||
default_groups = [Group::AUTO_GROUPS[:everyone]]
|
default_groups = [Group::AUTO_GROUPS[:everyone]]
|
||||||
end
|
end
|
||||||
|
|
||||||
persona.allowed_group_ids = from_setting(setting_name).first&.split("|") || default_groups
|
persona.allowed_group_ids = from_setting(setting_name) || default_groups
|
||||||
|
elsif persona_class == DiscourseAi::Personas::CustomPrompt
|
||||||
|
setting_name = "ai_helper_custom_prompts_allowed_groups"
|
||||||
|
default_groups = [Group::AUTO_GROUPS[:staff]]
|
||||||
|
persona.allowed_group_ids = from_setting(setting_name) || default_groups
|
||||||
else
|
else
|
||||||
persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
|
persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
|
||||||
end
|
end
|
||||||
|
|
|
@ -188,8 +188,7 @@ class DiscourseAi::Evals::Eval
|
||||||
end
|
end
|
||||||
|
|
||||||
def helper(llm, input:, name:, locale: nil)
|
def helper(llm, input:, name:, locale: nil)
|
||||||
completion_prompt = CompletionPrompt.find_by(name: name)
|
helper = DiscourseAi::AiHelper::Assistant.new(helper_llm: llm.llm_model)
|
||||||
helper = DiscourseAi::AiHelper::Assistant.new(helper_llm: llm.llm_proxy)
|
|
||||||
user = Discourse.system_user
|
user = Discourse.system_user
|
||||||
if locale
|
if locale
|
||||||
user = User.new
|
user = User.new
|
||||||
|
@ -202,7 +201,7 @@ class DiscourseAi::Evals::Eval
|
||||||
end
|
end
|
||||||
result =
|
result =
|
||||||
helper.generate_and_send_prompt(
|
helper.generate_and_send_prompt(
|
||||||
completion_prompt,
|
name,
|
||||||
input,
|
input,
|
||||||
current_user = user,
|
current_user = user,
|
||||||
_force_default_locale = false,
|
_force_default_locale = false,
|
||||||
|
|
|
@ -5,6 +5,16 @@ module DiscourseAi
|
||||||
class Assistant
|
class Assistant
|
||||||
IMAGE_CAPTION_MAX_WORDS = 50
|
IMAGE_CAPTION_MAX_WORDS = 50
|
||||||
|
|
||||||
|
TRANSLATE = "translate"
|
||||||
|
GENERATE_TITLES = "generate_titles"
|
||||||
|
PROOFREAD = "proofread"
|
||||||
|
MARKDOWN_TABLE = "markdown_table"
|
||||||
|
CUSTOM_PROMPT = "custom_prompt"
|
||||||
|
EXPLAIN = "explain"
|
||||||
|
ILLUSTRATE_POST = "illustrate_post"
|
||||||
|
REPLACE_DATES = "replace_dates"
|
||||||
|
IMAGE_CAPTION = "image_caption"
|
||||||
|
|
||||||
def self.prompt_cache
|
def self.prompt_cache
|
||||||
@prompt_cache ||= ::DiscourseAi::MultisiteHash.new("prompt_cache")
|
@prompt_cache ||= ::DiscourseAi::MultisiteHash.new("prompt_cache")
|
||||||
end
|
end
|
||||||
|
@ -18,58 +28,37 @@ module DiscourseAi
|
||||||
@image_caption_llm = image_caption_llm
|
@image_caption_llm = image_caption_llm
|
||||||
end
|
end
|
||||||
|
|
||||||
def helper_llm
|
|
||||||
@helper_llm || DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_model)
|
|
||||||
end
|
|
||||||
|
|
||||||
def image_caption_llm
|
|
||||||
@image_caption_llm ||
|
|
||||||
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_image_caption_model)
|
|
||||||
end
|
|
||||||
|
|
||||||
def available_prompts(user)
|
def available_prompts(user)
|
||||||
key = "prompt_cache_#{I18n.locale}"
|
key = "prompt_cache_#{I18n.locale}"
|
||||||
self
|
prompts = self.class.prompt_cache.fetch(key) { self.all_prompts }
|
||||||
.class
|
|
||||||
.prompt_cache
|
|
||||||
.fetch(key) do
|
|
||||||
prompts = CompletionPrompt.where(enabled: true)
|
|
||||||
|
|
||||||
# Hide illustrate_post if disabled
|
prompts
|
||||||
prompts =
|
.map do |prompt|
|
||||||
prompts.where.not(
|
next if !user.in_any_groups?(prompt[:allowed_group_ids])
|
||||||
name: "illustrate_post",
|
|
||||||
) if SiteSetting.ai_helper_illustrate_post_model == "disabled"
|
|
||||||
|
|
||||||
prompts =
|
if prompt[:name] == ILLUSTRATE_POST &&
|
||||||
prompts.map do |prompt|
|
SiteSetting.ai_helper_illustrate_post_model == "disabled"
|
||||||
if prompt.name == "translate"
|
next
|
||||||
locale = user.effective_locale
|
end
|
||||||
locale_hash =
|
|
||||||
LocaleSiteSetting.language_names[locale] ||
|
|
||||||
LocaleSiteSetting.language_names[locale.split("_")[0]]
|
|
||||||
translation =
|
|
||||||
I18n.t(
|
|
||||||
"discourse_ai.ai_helper.prompts.translate",
|
|
||||||
language: locale_hash["nativeName"],
|
|
||||||
) || prompt.translated_name || prompt.name
|
|
||||||
else
|
|
||||||
translation =
|
|
||||||
I18n.t("discourse_ai.ai_helper.prompts.#{prompt.name}", default: nil) ||
|
|
||||||
prompt.translated_name || prompt.name
|
|
||||||
end
|
|
||||||
|
|
||||||
{
|
# We cannot cache this. It depends on the user's effective_locale.
|
||||||
id: prompt.id,
|
if prompt[:name] == TRANSLATE
|
||||||
name: prompt.name,
|
locale = user.effective_locale
|
||||||
translated_name: translation,
|
locale_hash =
|
||||||
prompt_type: prompt.prompt_type,
|
LocaleSiteSetting.language_names[locale] ||
|
||||||
icon: icon_map(prompt.name),
|
LocaleSiteSetting.language_names[locale.split("_")[0]]
|
||||||
location: location_map(prompt.name),
|
translation =
|
||||||
}
|
I18n.t(
|
||||||
end
|
"discourse_ai.ai_helper.prompts.translate",
|
||||||
prompts
|
language: locale_hash["nativeName"],
|
||||||
|
) || prompt[:name]
|
||||||
|
|
||||||
|
prompt.merge(translated_name: translation)
|
||||||
|
else
|
||||||
|
prompt
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_locale_instructions(user = nil, force_default_locale)
|
def custom_locale_instructions(user = nil, force_default_locale)
|
||||||
|
@ -85,26 +74,14 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def localize_prompt!(prompt, user = nil, force_default_locale: false)
|
def attach_user_context(context, user = nil, force_default_locale: false)
|
||||||
locale_instructions = custom_locale_instructions(user, force_default_locale)
|
locale = SiteSetting.default_locale
|
||||||
if locale_instructions
|
locale = user.effective_locale if user && !force_default_locale
|
||||||
prompt.messages[0][:content] = prompt.messages[0][:content] + locale_instructions
|
locale_hash = LocaleSiteSetting.language_names[locale]
|
||||||
end
|
|
||||||
|
|
||||||
if prompt.messages[0][:content].include?("%LANGUAGE%")
|
context.user_language = "#{locale_hash["name"]}"
|
||||||
locale = SiteSetting.default_locale
|
|
||||||
|
|
||||||
locale = user.effective_locale if user && !force_default_locale
|
if user
|
||||||
|
|
||||||
locale_hash = LocaleSiteSetting.language_names[locale]
|
|
||||||
|
|
||||||
prompt.messages[0][:content] = prompt.messages[0][:content].gsub(
|
|
||||||
"%LANGUAGE%",
|
|
||||||
"#{locale_hash["name"]}",
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if user && prompt.messages[0][:content].include?("{{temporal_context}}")
|
|
||||||
timezone = user.user_option.timezone || "UTC"
|
timezone = user.user_option.timezone || "UTC"
|
||||||
current_time = Time.now.in_time_zone(timezone)
|
current_time = Time.now.in_time_zone(timezone)
|
||||||
|
|
||||||
|
@ -117,48 +94,85 @@ module DiscourseAi
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt.messages[0][:content] = prompt.messages[0][:content].gsub(
|
context.temporal_context = temporal_context.to_json
|
||||||
"{{temporal_context}}",
|
|
||||||
temporal_context.to_json,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt.messages.each do |message|
|
|
||||||
message[:content] = DateFormatter.process_date_placeholders(message[:content], user)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_prompt(completion_prompt, input, user, force_default_locale: false, &block)
|
def generate_prompt(
|
||||||
llm = helper_llm
|
helper_mode,
|
||||||
prompt = completion_prompt.messages_with_input(input)
|
input,
|
||||||
localize_prompt!(prompt, user, force_default_locale: force_default_locale)
|
user,
|
||||||
|
force_default_locale: false,
|
||||||
|
custom_prompt: nil,
|
||||||
|
&block
|
||||||
|
)
|
||||||
|
bot = build_bot(helper_mode, user)
|
||||||
|
|
||||||
llm.generate(
|
user_input = "<input>#{input}</input>"
|
||||||
prompt,
|
if helper_mode == CUSTOM_PROMPT && custom_prompt.present?
|
||||||
user: user,
|
user_input = "<input>#{custom_prompt}:\n#{input}</input>"
|
||||||
temperature: completion_prompt.temperature,
|
end
|
||||||
stop_sequences: completion_prompt.stop_sequences,
|
|
||||||
feature_name: "ai_helper",
|
context =
|
||||||
&block
|
DiscourseAi::Personas::BotContext.new(
|
||||||
)
|
user: user,
|
||||||
|
skip_tool_details: true,
|
||||||
|
feature_name: "ai_helper",
|
||||||
|
messages: [{ type: :user, content: user_input }],
|
||||||
|
format_dates: helper_mode == REPLACE_DATES,
|
||||||
|
custom_instructions: custom_locale_instructions(user, force_default_locale),
|
||||||
|
)
|
||||||
|
context = attach_user_context(context, user, force_default_locale: force_default_locale)
|
||||||
|
|
||||||
|
helper_response = +""
|
||||||
|
|
||||||
|
buffer_blk =
|
||||||
|
Proc.new do |partial, _, type|
|
||||||
|
if type == :structured_output
|
||||||
|
json_summary_schema_key = bot.persona.response_format&.first.to_h
|
||||||
|
helper_chunk = partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
|
||||||
|
|
||||||
|
if !helper_chunk.nil? && !helper_chunk.empty?
|
||||||
|
helper_response << helper_chunk
|
||||||
|
block.call(helper_chunk) if block
|
||||||
|
end
|
||||||
|
elsif type.blank?
|
||||||
|
# Assume response is a regular completion.
|
||||||
|
helper_response << helper_chunk
|
||||||
|
block.call(helper_chunk) if block
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
bot.reply(context, &buffer_blk)
|
||||||
|
|
||||||
|
helper_response
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_and_send_prompt(completion_prompt, input, user, force_default_locale: false)
|
def generate_and_send_prompt(
|
||||||
completion_result =
|
helper_mode,
|
||||||
|
input,
|
||||||
|
user,
|
||||||
|
force_default_locale: false,
|
||||||
|
custom_prompt: nil
|
||||||
|
)
|
||||||
|
helper_response =
|
||||||
generate_prompt(
|
generate_prompt(
|
||||||
completion_prompt,
|
helper_mode,
|
||||||
input,
|
input,
|
||||||
user,
|
user,
|
||||||
force_default_locale: force_default_locale,
|
force_default_locale: force_default_locale,
|
||||||
|
custom_prompt: custom_prompt,
|
||||||
)
|
)
|
||||||
result = { type: completion_prompt.prompt_type }
|
result = { type: prompt_type(helper_mode) }
|
||||||
|
|
||||||
result[:suggestions] = (
|
result[:suggestions] = (
|
||||||
if completion_prompt.list?
|
if result[:type] == :list
|
||||||
parse_list(completion_result).map { |suggestion| sanitize_result(suggestion) }
|
parse_list(helper_response).map { |suggestion| sanitize_result(suggestion) }
|
||||||
else
|
else
|
||||||
sanitized = sanitize_result(completion_result)
|
sanitized = sanitize_result(helper_response)
|
||||||
result[:diff] = parse_diff(input, sanitized) if completion_prompt.diff?
|
result[:diff] = parse_diff(input, sanitized) if result[:type] == :diff
|
||||||
[sanitized]
|
[sanitized]
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
@ -167,25 +181,28 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def stream_prompt(
|
def stream_prompt(
|
||||||
completion_prompt,
|
helper_mode,
|
||||||
input,
|
input,
|
||||||
user,
|
user,
|
||||||
channel,
|
channel,
|
||||||
force_default_locale: false,
|
force_default_locale: false,
|
||||||
client_id: nil
|
client_id: nil,
|
||||||
|
custom_prompt: nil
|
||||||
)
|
)
|
||||||
streamed_diff = +""
|
streamed_diff = +""
|
||||||
streamed_result = +""
|
streamed_result = +""
|
||||||
start = Time.now
|
start = Time.now
|
||||||
|
type = prompt_type(helper_mode)
|
||||||
|
|
||||||
generate_prompt(
|
generate_prompt(
|
||||||
completion_prompt,
|
helper_mode,
|
||||||
input,
|
input,
|
||||||
user,
|
user,
|
||||||
force_default_locale: force_default_locale,
|
force_default_locale: force_default_locale,
|
||||||
) do |partial_response, cancel_function|
|
custom_prompt: custom_prompt,
|
||||||
|
) do |partial_response|
|
||||||
streamed_result << partial_response
|
streamed_result << partial_response
|
||||||
streamed_diff = parse_diff(input, partial_response) if completion_prompt.diff?
|
streamed_diff = parse_diff(input, partial_response) if type == :diff
|
||||||
|
|
||||||
# Throttle updates and check for safe stream points
|
# Throttle updates and check for safe stream points
|
||||||
if (streamed_result.length > 10 && (Time.now - start > 0.3)) || Rails.env.test?
|
if (streamed_result.length > 10 && (Time.now - start > 0.3)) || Rails.env.test?
|
||||||
|
@ -197,7 +214,7 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
final_diff = parse_diff(input, streamed_result) if completion_prompt.diff?
|
final_diff = parse_diff(input, streamed_result) if type == :diff
|
||||||
|
|
||||||
sanitized_result = sanitize_result(streamed_result)
|
sanitized_result = sanitize_result(streamed_result)
|
||||||
if sanitized_result.present?
|
if sanitized_result.present?
|
||||||
|
@ -211,33 +228,124 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_image_caption(upload, user)
|
def generate_image_caption(upload, user)
|
||||||
prompt =
|
bot = build_bot(IMAGE_CAPTION, user)
|
||||||
DiscourseAi::Completions::Prompt.new(
|
force_default_locale = false
|
||||||
"You are a bot specializing in image captioning.",
|
|
||||||
|
context =
|
||||||
|
DiscourseAi::Personas::BotContext.new(
|
||||||
|
user: user,
|
||||||
|
skip_tool_details: true,
|
||||||
|
feature_name: IMAGE_CAPTION,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
type: :user,
|
type: :user,
|
||||||
content: [
|
content: ["Describe this image in a single sentence.", { upload_id: upload.id }],
|
||||||
"Describe this image in a single sentence#{custom_locale_instructions(user)}",
|
|
||||||
{ upload_id: upload.id },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
custom_instructions: custom_locale_instructions(user, force_default_locale),
|
||||||
)
|
)
|
||||||
|
|
||||||
raw_caption =
|
structured_output = nil
|
||||||
image_caption_llm.generate(
|
|
||||||
prompt,
|
buffer_blk =
|
||||||
user: user,
|
Proc.new do |partial, _, type|
|
||||||
max_tokens: 1024,
|
if type == :structured_output
|
||||||
feature_name: "image_caption",
|
structured_output = partial
|
||||||
)
|
json_summary_schema_key = bot.persona.response_format&.first.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
bot.reply(context, llm_args: { max_tokens: 1024 }, &buffer_blk)
|
||||||
|
|
||||||
|
raw_caption = ""
|
||||||
|
|
||||||
|
if structured_output
|
||||||
|
json_summary_schema_key = bot.persona.response_format&.first.to_h
|
||||||
|
raw_caption =
|
||||||
|
structured_output.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
raw_caption.delete("|").squish.truncate_words(IMAGE_CAPTION_MAX_WORDS)
|
raw_caption.delete("|").squish.truncate_words(IMAGE_CAPTION_MAX_WORDS)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_bot(helper_mode, user)
|
||||||
|
persona_id = personas_prompt_map(include_image_caption: true).invert[helper_mode]
|
||||||
|
raise Discourse::InvalidParameters.new(:mode) if persona_id.blank?
|
||||||
|
|
||||||
|
persona_klass = AiPersona.find_by(id: persona_id)&.class_instance
|
||||||
|
return if persona_klass.nil?
|
||||||
|
|
||||||
|
llm_model = find_ai_helper_model(helper_mode, persona_klass)
|
||||||
|
|
||||||
|
DiscourseAi::Personas::Bot.as(user, persona: persona_klass.new, model: llm_model)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Priorities are:
|
||||||
|
# 1. Persona's default LLM
|
||||||
|
# 2. Hidden `ai_helper_model` setting, or `ai_helper_image_caption_model` for image_caption.
|
||||||
|
# 3. Newest LLM config
|
||||||
|
def find_ai_helper_model(helper_mode, persona_klass)
|
||||||
|
model_id = persona_klass.default_llm_id
|
||||||
|
|
||||||
|
if !model_id
|
||||||
|
if helper_mode == IMAGE_CAPTION
|
||||||
|
model_id = @helper_llm || SiteSetting.ai_helper_image_caption_model&.split(":")&.last
|
||||||
|
else
|
||||||
|
model_id = @image_caption_llm || SiteSetting.ai_helper_model&.split(":")&.last
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if model_id.present?
|
||||||
|
LlmModel.find_by(id: model_id)
|
||||||
|
else
|
||||||
|
LlmModel.last
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def personas_prompt_map(include_image_caption: false)
|
||||||
|
map = {
|
||||||
|
SiteSetting.ai_helper_translator_persona.to_i => TRANSLATE,
|
||||||
|
SiteSetting.ai_helper_title_suggestions_persona.to_i => GENERATE_TITLES,
|
||||||
|
SiteSetting.ai_helper_proofreader_persona.to_i => PROOFREAD,
|
||||||
|
SiteSetting.ai_helper_markdown_tables_persona.to_i => MARKDOWN_TABLE,
|
||||||
|
SiteSetting.ai_helper_custom_prompt_persona.to_i => CUSTOM_PROMPT,
|
||||||
|
SiteSetting.ai_helper_explain_persona.to_i => EXPLAIN,
|
||||||
|
SiteSetting.ai_helper_post_illustrator_persona.to_i => ILLUSTRATE_POST,
|
||||||
|
SiteSetting.ai_helper_smart_dates_persona.to_i => REPLACE_DATES,
|
||||||
|
}
|
||||||
|
|
||||||
|
if include_image_caption
|
||||||
|
image_caption_persona = SiteSetting.ai_helper_image_caption_persona.to_i
|
||||||
|
map[image_caption_persona] = IMAGE_CAPTION if image_caption_persona
|
||||||
|
end
|
||||||
|
|
||||||
|
map
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_prompts
|
||||||
|
AiPersona
|
||||||
|
.where(id: personas_prompt_map.keys)
|
||||||
|
.map do |ai_persona|
|
||||||
|
prompt_name = personas_prompt_map[ai_persona.id]
|
||||||
|
|
||||||
|
if prompt_name
|
||||||
|
{
|
||||||
|
name: prompt_name,
|
||||||
|
translated_name:
|
||||||
|
I18n.t("discourse_ai.ai_helper.prompts.#{prompt_name}", default: nil) ||
|
||||||
|
prompt_name,
|
||||||
|
prompt_type: prompt_type(prompt_name),
|
||||||
|
icon: icon_map(prompt_name),
|
||||||
|
location: location_map(prompt_name),
|
||||||
|
allowed_group_ids: ai_persona.allowed_group_ids,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
.compact
|
||||||
|
end
|
||||||
|
|
||||||
SANITIZE_REGEX_STR =
|
SANITIZE_REGEX_STR =
|
||||||
%w[term context topic replyTo input output result]
|
%w[term context topic replyTo input output result]
|
||||||
.map { |tag| "<#{tag}>\\n?|\\n?</#{tag}>" }
|
.map { |tag| "<#{tag}>\\n?|\\n?</#{tag}>" }
|
||||||
|
@ -268,25 +376,21 @@ module DiscourseAi
|
||||||
|
|
||||||
def icon_map(name)
|
def icon_map(name)
|
||||||
case name
|
case name
|
||||||
when "translate"
|
when TRANSLATE
|
||||||
"language"
|
"language"
|
||||||
when "generate_titles"
|
when GENERATE_TITLES
|
||||||
"heading"
|
"heading"
|
||||||
when "proofread"
|
when PROOFREAD
|
||||||
"spell-check"
|
"spell-check"
|
||||||
when "markdown_table"
|
when MARKDOWN_TABLE
|
||||||
"table"
|
"table"
|
||||||
when "tone"
|
when CUSTOM_PROMPT
|
||||||
"microphone"
|
|
||||||
when "custom_prompt"
|
|
||||||
"comment"
|
"comment"
|
||||||
when "rewrite"
|
when EXPLAIN
|
||||||
"pen"
|
|
||||||
when "explain"
|
|
||||||
"question"
|
"question"
|
||||||
when "illustrate_post"
|
when ILLUSTRATE_POST
|
||||||
"images"
|
"images"
|
||||||
when "replace_dates"
|
when REPLACE_DATES
|
||||||
"calendar-days"
|
"calendar-days"
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
|
@ -295,33 +399,37 @@ module DiscourseAi
|
||||||
|
|
||||||
def location_map(name)
|
def location_map(name)
|
||||||
case name
|
case name
|
||||||
when "translate"
|
when TRANSLATE
|
||||||
%w[composer post]
|
%w[composer post]
|
||||||
when "generate_titles"
|
when GENERATE_TITLES
|
||||||
%w[composer]
|
%w[composer]
|
||||||
when "proofread"
|
when PROOFREAD
|
||||||
%w[composer post]
|
%w[composer post]
|
||||||
when "markdown_table"
|
when MARKDOWN_TABLE
|
||||||
%w[composer]
|
%w[composer]
|
||||||
when "tone"
|
when CUSTOM_PROMPT
|
||||||
%w[composer]
|
|
||||||
when "custom_prompt"
|
|
||||||
%w[composer post]
|
%w[composer post]
|
||||||
when "rewrite"
|
when EXPLAIN
|
||||||
%w[composer]
|
|
||||||
when "explain"
|
|
||||||
%w[post]
|
%w[post]
|
||||||
when "summarize"
|
when ILLUSTRATE_POST
|
||||||
%w[post]
|
|
||||||
when "illustrate_post"
|
|
||||||
%w[composer]
|
%w[composer]
|
||||||
when "replace_dates"
|
when REPLACE_DATES
|
||||||
%w[composer]
|
%w[composer]
|
||||||
else
|
else
|
||||||
%w[]
|
%w[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prompt_type(prompt_name)
|
||||||
|
if [PROOFREAD, MARKDOWN_TABLE, REPLACE_DATES, CUSTOM_PROMPT].include?(prompt_name)
|
||||||
|
return :diff
|
||||||
|
end
|
||||||
|
|
||||||
|
return :list if [ILLUSTRATE_POST, GENERATE_TITLES].include?(prompt_name)
|
||||||
|
|
||||||
|
:text
|
||||||
|
end
|
||||||
|
|
||||||
def parse_diff(text, suggestion)
|
def parse_diff(text, suggestion)
|
||||||
cooked_text = PrettyText.cook(text)
|
cooked_text = PrettyText.cook(text)
|
||||||
cooked_suggestion = PrettyText.cook(suggestion)
|
cooked_suggestion = PrettyText.cook(suggestion)
|
||||||
|
|
|
@ -41,8 +41,6 @@ module DiscourseAi
|
||||||
"The number of completions you requested exceed the number of canned responses"
|
"The number of completions you requested exceed the number of canned responses"
|
||||||
end
|
end
|
||||||
|
|
||||||
response = as_structured_output(response) if model_params[:response_format].present?
|
|
||||||
|
|
||||||
raise response if response.is_a?(StandardError)
|
raise response if response.is_a?(StandardError)
|
||||||
|
|
||||||
@completions += 1
|
@completions += 1
|
||||||
|
@ -57,8 +55,9 @@ module DiscourseAi
|
||||||
yield(response, cancel_fn)
|
yield(response, cancel_fn)
|
||||||
elsif is_thinking?(response)
|
elsif is_thinking?(response)
|
||||||
yield(response, cancel_fn)
|
yield(response, cancel_fn)
|
||||||
elsif is_structured_output?(response)
|
elsif model_params[:response_format].present?
|
||||||
yield(response, cancel_fn)
|
structured_output = as_structured_output(response)
|
||||||
|
yield(structured_output, cancel_fn)
|
||||||
else
|
else
|
||||||
response.each_char do |char|
|
response.each_char do |char|
|
||||||
break if cancelled
|
break if cancelled
|
||||||
|
@ -69,6 +68,7 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
response = response.first if response.is_a?(Array) && response.length == 1
|
response = response.first if response.is_a?(Array) && response.length == 1
|
||||||
|
response = as_structured_output(response) if model_params[:response_format].present?
|
||||||
|
|
||||||
response
|
response
|
||||||
end
|
end
|
||||||
|
@ -87,10 +87,6 @@ module DiscourseAi
|
||||||
response.is_a?(DiscourseAi::Completions::ToolCall)
|
response.is_a?(DiscourseAi::Completions::ToolCall)
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_structured_output?(response)
|
|
||||||
response.is_a?(DiscourseAi::Completions::StructuredOutput)
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_structured_output(response)
|
def as_structured_output(response)
|
||||||
schema_properties = model_params[:response_format].dig(:json_schema, :schema, :properties)
|
schema_properties = model_params[:response_format].dig(:json_schema, :schema, :properties)
|
||||||
return response if schema_properties.blank?
|
return response if schema_properties.blank?
|
||||||
|
|
|
@ -10,7 +10,7 @@ module DiscourseAi
|
||||||
def valid_value?(val)
|
def valid_value?(val)
|
||||||
return true if val == "f"
|
return true if val == "f"
|
||||||
|
|
||||||
if @opts[:name] == :ai_summarization_enabled
|
if @opts[:name] == :ai_summarization_enabled || @opts[:name] == :ai_helper_enabled
|
||||||
has_llms = LlmModel.count > 0
|
has_llms = LlmModel.count > 0
|
||||||
@no_llms_configured = !has_llms
|
@no_llms_configured = !has_llms
|
||||||
has_llms
|
has_llms
|
||||||
|
|
|
@ -18,7 +18,10 @@ module DiscourseAi
|
||||||
:feature_name,
|
:feature_name,
|
||||||
:resource_url,
|
:resource_url,
|
||||||
:cancel_manager,
|
:cancel_manager,
|
||||||
:inferred_concepts
|
:inferred_concepts,
|
||||||
|
:format_dates,
|
||||||
|
:temporal_context,
|
||||||
|
:user_language
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
post: nil,
|
post: nil,
|
||||||
|
@ -37,13 +40,15 @@ module DiscourseAi
|
||||||
feature_name: "bot",
|
feature_name: "bot",
|
||||||
resource_url: nil,
|
resource_url: nil,
|
||||||
cancel_manager: nil,
|
cancel_manager: nil,
|
||||||
inferred_concepts: []
|
inferred_concepts: [],
|
||||||
|
format_dates: false
|
||||||
)
|
)
|
||||||
@participants = participants
|
@participants = participants
|
||||||
@user = user
|
@user = user
|
||||||
@skip_tool_details = skip_tool_details
|
@skip_tool_details = skip_tool_details
|
||||||
@messages = messages
|
@messages = messages
|
||||||
@custom_instructions = custom_instructions
|
@custom_instructions = custom_instructions
|
||||||
|
@format_dates = format_dates
|
||||||
|
|
||||||
@message_id = message_id
|
@message_id = message_id
|
||||||
@channel_id = channel_id
|
@channel_id = channel_id
|
||||||
|
@ -78,6 +83,8 @@ module DiscourseAi
|
||||||
participants
|
participants
|
||||||
resource_url
|
resource_url
|
||||||
inferred_concepts
|
inferred_concepts
|
||||||
|
user_language
|
||||||
|
temporal_context
|
||||||
]
|
]
|
||||||
|
|
||||||
def lookup_template_param(key)
|
def lookup_template_param(key)
|
||||||
|
@ -125,6 +132,8 @@ module DiscourseAi
|
||||||
feature_name: @feature_name,
|
feature_name: @feature_name,
|
||||||
resource_url: @resource_url,
|
resource_url: @resource_url,
|
||||||
inferred_concepts: @inferred_concepts,
|
inferred_concepts: @inferred_concepts,
|
||||||
|
user_language: @user_language,
|
||||||
|
temporal_context: @temporal_context,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class CustomPrompt < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
You are a helpful assistant. I will give you instructions inside <input></input> XML tags.
|
||||||
|
You will look at them and reply with a result.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class ImageCaptioner < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
"You are a bot specializing in image captioning."
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,59 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class MarkdownTableGenerator < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
You are a markdown table formatter, I will provide you text inside <input></input> XML tags and you will format it into a markdown table
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def temperature
|
||||||
|
0.5
|
||||||
|
end
|
||||||
|
|
||||||
|
def examples
|
||||||
|
[
|
||||||
|
["<input>sam,joe,jane\nage: 22| 10|11</input>", { output: <<~TEXT }.to_json],
|
||||||
|
| | sam | joe | jane |
|
||||||
|
|---|---|---|---|
|
||||||
|
| age | 22 | 10 | 11 |
|
||||||
|
TEXT
|
||||||
|
[<<~TEXT, { output: <<~TEXT }.to_json],
|
||||||
|
<input>
|
||||||
|
sam: speed 100, age 22
|
||||||
|
jane: age 10
|
||||||
|
fred: height 22
|
||||||
|
</input>
|
||||||
|
TEXT
|
||||||
|
| | speed | age | height |
|
||||||
|
|---|---|---|---|
|
||||||
|
| sam | 100 | 22 | - |
|
||||||
|
| jane | - | 10 | - |
|
||||||
|
| fred | - | - | 22 |
|
||||||
|
TEXT
|
||||||
|
[<<~TEXT, { output: <<~TEXT }.to_json],
|
||||||
|
<input>
|
||||||
|
chrome 22ms (first load 10ms)
|
||||||
|
firefox 10ms (first load: 9ms)
|
||||||
|
</input>
|
||||||
|
TEXT
|
||||||
|
| Browser | Load Time (ms) | First Load Time (ms) |
|
||||||
|
|---|---|---|
|
||||||
|
| Chrome | 22 | 10 |
|
||||||
|
| Firefox | 10 | 9 |
|
||||||
|
TEXT
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -55,6 +55,15 @@ module DiscourseAi
|
||||||
ConceptFinder => -15,
|
ConceptFinder => -15,
|
||||||
ConceptMatcher => -16,
|
ConceptMatcher => -16,
|
||||||
ConceptDeduplicator => -17,
|
ConceptDeduplicator => -17,
|
||||||
|
CustomPrompt => -18,
|
||||||
|
SmartDates => -19,
|
||||||
|
MarkdownTableGenerator => -20,
|
||||||
|
PostIllustrator => -21,
|
||||||
|
Proofreader => -22,
|
||||||
|
TitlesGenerator => -23,
|
||||||
|
Tutor => -24,
|
||||||
|
Translator => -25,
|
||||||
|
ImageCaptioner => -26,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -260,10 +269,15 @@ module DiscourseAi
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def replace_placeholders(content, context)
|
def replace_placeholders(content, context)
|
||||||
content.gsub(/\{(\w+)\}/) do |match|
|
replaced =
|
||||||
found = context.lookup_template_param(match[1..-2])
|
content.gsub(/\{(\w+)\}/) do |match|
|
||||||
found.nil? ? match : found.to_s
|
found = context.lookup_template_param(match[1..-2])
|
||||||
end
|
found.nil? ? match : found.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
return replaced if !context.format_dates
|
||||||
|
|
||||||
|
::DiscourseAi::AiHelper::DateFormatter.process_date_placeholders(replaced, context.user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:)
|
def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class PostIllustrator < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
Provide me a StableDiffusion prompt to generate an image that illustrates the following post in 40 words or less, be creative.
|
||||||
|
You'll find the post between <input></input> XML tags.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class Proofreader < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
You are a markdown proofreader. You correct egregious typos and phrasing issues but keep the user's original voice.
|
||||||
|
You do not touch code blocks. I will provide you with text to proofread. If nothing needs fixing, then you will echo the text back.
|
||||||
|
You will find the text between <input></input> XML tags.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def examples
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"<input></input>",
|
||||||
|
{ output: "" }.to_json,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"<input>The rain in spain stays mainly in the plane.</input>",
|
||||||
|
{ output: "The rain in Spain, stays mainly in the Plane." }.to_json,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"<input>The rain in Spain, stays mainly in the Plane.</input>",
|
||||||
|
{ output: "The rain in Spain, stays mainly in the Plane." }.to_json,
|
||||||
|
],
|
||||||
|
[<<~TEXT, { output: <<~TEXT }.to_json],
|
||||||
|
<input>
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
Sometimes the logo isn't changing automatically when color scheme changes.
|
||||||
|
|
||||||
|

|
||||||
|
</input>
|
||||||
|
TEXT
|
||||||
|
Hello,
|
||||||
|
Sometimes the logo does not change automatically when the color scheme changes.
|
||||||
|

|
||||||
|
TEXT
|
||||||
|
[<<~TEXT, { output: <<~TEXT }.to_json],
|
||||||
|
<input>
|
||||||
|
Any ideas what is wrong with this peace of cod?
|
||||||
|
> This quot contains a typo
|
||||||
|
```ruby
|
||||||
|
# this has speling mistakes
|
||||||
|
testin.atypo = 11
|
||||||
|
baad = "bad"
|
||||||
|
```
|
||||||
|
</input>
|
||||||
|
TEXT
|
||||||
|
Any ideas what is wrong with this piece of code?
|
||||||
|
> This quot contains a typo
|
||||||
|
```ruby
|
||||||
|
# This has spelling mistakes
|
||||||
|
testing.a_typo = 11
|
||||||
|
bad = "bad"
|
||||||
|
```
|
||||||
|
TEXT
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,63 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class SmartDates < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
You are a date and time formatter for Discourse posts. Convert natural language time references into date placeholders.
|
||||||
|
Do not modify any markdown, code blocks, or existing date formats.
|
||||||
|
|
||||||
|
Here's the temporal context:
|
||||||
|
{temporal_context}
|
||||||
|
|
||||||
|
Available date placeholder formats:
|
||||||
|
- Simple day without time: {{date:1}} for tomorrow, {{date:7}} for a week from today
|
||||||
|
- Specific time: {{datetime:2pm+1}} for 2 PM tomorrow
|
||||||
|
- Time range: {{datetime:2pm+1:4pm+1}} for tomorrow 2 PM to 4 PM
|
||||||
|
|
||||||
|
You will find the text between <input></input> XML tags.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def examples
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"<input>The meeting is at 2pm tomorrow</input>",
|
||||||
|
{ output: "The meeting is at {{datetime:2pm+1}}" }.to_json,
|
||||||
|
],
|
||||||
|
["<input>Due in 3 days</input>", { output: "Due {{date:3}}" }.to_json],
|
||||||
|
[
|
||||||
|
"<input>Meeting next Tuesday at 2pm</input>",
|
||||||
|
{ output: "Meeting {{next_week:tuesday-2pm}}" }.to_json,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"<input>Meeting from 2pm to 4pm tomorrow</input>",
|
||||||
|
{ output: "Meeting {{datetime:2pm+1:4pm+1}}" }.to_json,
|
||||||
|
],
|
||||||
|
[<<~TEXT, { output: <<~TEXT }.to_json],
|
||||||
|
<input>Meeting notes for tomorrow:
|
||||||
|
* Action items in `config.rb`
|
||||||
|
* Review PR #1234
|
||||||
|
* Deadline is 5pm
|
||||||
|
* Check [this link](https://example.com)</input>
|
||||||
|
TEXT
|
||||||
|
Meeting notes for {{date:1}}:
|
||||||
|
* Action items in `config.rb`
|
||||||
|
* Review PR #1234
|
||||||
|
* Deadline is {{datetime:5pm+1}}
|
||||||
|
* Check [this link](https://example.com)
|
||||||
|
TEXT
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,7 +21,7 @@ module DiscourseAi
|
||||||
- Example: links to the 3rd and 6th posts by sam: sam ([#3]({resource_url}/3), [#6]({resource_url}/6))
|
- Example: links to the 3rd and 6th posts by sam: sam ([#3]({resource_url}/3), [#6]({resource_url}/6))
|
||||||
- Example: link to the 6th post by jane: [agreed with]({resource_url}/6)
|
- Example: link to the 6th post by jane: [agreed with]({resource_url}/6)
|
||||||
- Example: link to the 13th post by joe: [joe]({resource_url}/13)
|
- Example: link to the 13th post by joe: [joe]({resource_url}/13)
|
||||||
- When formatting usernames either use @USERNAME OR [USERNAME]({resource_url}/POST_NUMBER)
|
- When formatting usernames use [USERNAME]({resource_url}/POST_NUMBER)
|
||||||
|
|
||||||
Format your response as a JSON object with a single key named "summary", which has the summary as the value.
|
Format your response as a JSON object with a single key named "summary", which has the summary as the value.
|
||||||
Your output should be in the following format:
|
Your output should be in the following format:
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class TitlesGenerator < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
I want you to act as a title generator for written pieces. I will provide you with a text,
|
||||||
|
and you will generate five titles. Please keep the title concise and under 20 words,
|
||||||
|
and ensure that the meaning is maintained. Replies will utilize the language type of the topic.
|
||||||
|
I want you to only reply the list of options and nothing else, do not write explanations.
|
||||||
|
Never ever use colons in the title. Always use sentence case, using a capital letter at
|
||||||
|
the start of the title, never start the title with a lower case letter. Proper nouns in the title
|
||||||
|
can have a capital letter, and acronyms like LLM can use capital letters. Format some titles
|
||||||
|
as questions, some as statements. Make sure to use question marks if the title is a question.
|
||||||
|
You will find the text between <input></input> XML tags.
|
||||||
|
Wrap each title between <item></item> XML tags.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def examples
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"<input>In the labyrinth of time, a solitary horse, etched in gold by the setting sun, embarked on an infinite journey.</input>",
|
||||||
|
"<item>The solitary horse</item><item>The horse etched in gold</item><item>A horse's infinite journey</item><item>A horse lost in time</item><item>A horse's last ride</item>",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -47,6 +47,7 @@ module DiscourseAi
|
||||||
- status:open|closed|archived|noreplies|single_user - topic status filters
|
- status:open|closed|archived|noreplies|single_user - topic status filters
|
||||||
- max_results:N - limit results (per OR group)
|
- max_results:N - limit results (per OR group)
|
||||||
- order:latest|oldest|latest_topic|oldest_topic|likes - sort order
|
- order:latest|oldest|latest_topic|oldest_topic|likes - sort order
|
||||||
|
#{assign_tip}
|
||||||
|
|
||||||
**OR Logic:** Each OR group processes independently - filters don't cross boundaries.
|
**OR Logic:** Each OR group processes independently - filters don't cross boundaries.
|
||||||
|
|
||||||
|
@ -56,6 +57,16 @@ module DiscourseAi
|
||||||
TEXT
|
TEXT
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assign_tip
|
||||||
|
if SiteSetting.respond_to?(:assign_enabled) && SiteSetting.assign_enabled
|
||||||
|
(<<~TEXT).strip
|
||||||
|
assigned_to:username or assigned_to:username1,username2 - topics assigned to a specific user
|
||||||
|
assigned_to:* - topics assigned to any user
|
||||||
|
assigned_to:nobody - topics not assigned to any user
|
||||||
|
TEXT
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
"researcher"
|
"researcher"
|
||||||
end
|
end
|
||||||
|
@ -113,6 +124,8 @@ module DiscourseAi
|
||||||
else
|
else
|
||||||
process_filter(filter, goals, post, &blk)
|
process_filter(filter, goals, post, &blk)
|
||||||
end
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
{ error: "Error processing research: #{e.message}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def details
|
def details
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class Translator < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
I want you to act as an {user_language} translator, spelling corrector and improver. I will write to you
|
||||||
|
in any language and you will detect the language, translate it and answer in the corrected and
|
||||||
|
improved version of my text, in {user_language}. I want you to replace my simplified A0-level words and
|
||||||
|
sentences with more beautiful and elegant, upper level {user_language} words and sentences.
|
||||||
|
Keep the meaning same, but make them more literary. I want you to only reply the correction,
|
||||||
|
the improvements and nothing else, do not write explanations.
|
||||||
|
You will find the text between <input></input> XML tags.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def temperature
|
||||||
|
0.2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Personas
|
||||||
|
class Tutor < Persona
|
||||||
|
def self.default_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_prompt
|
||||||
|
<<~PROMPT.strip
|
||||||
|
You are a tutor explaining a term to a student in a specific context.
|
||||||
|
|
||||||
|
I will provide everything you need to know inside <input> tags, which consists of the term I want you
|
||||||
|
to explain inside <term> tags, the context of where it was used inside <context> tags, the title of
|
||||||
|
the topic where it was used inside <topic> tags, and optionally, the previous post in the conversation
|
||||||
|
in <replyTo> tags.
|
||||||
|
|
||||||
|
Using all this information, write a paragraph with a brief explanation
|
||||||
|
of what the term means. Format the response using Markdown. Reply only with the explanation and
|
||||||
|
nothing more.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_format
|
||||||
|
[{ "key" => "output", "type" => "string" }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -118,7 +118,7 @@ module DiscourseAi
|
||||||
partial_summary =
|
partial_summary =
|
||||||
partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
|
partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
|
||||||
|
|
||||||
if partial_summary.present?
|
if !partial_summary.nil? && !partial_summary.empty?
|
||||||
summary << partial_summary
|
summary << partial_summary
|
||||||
on_partial_blk.call(partial_summary) if on_partial_blk
|
on_partial_blk.call(partial_summary) if on_partial_blk
|
||||||
end
|
end
|
||||||
|
|
|
@ -162,6 +162,34 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.assign_allowed?(guardian)
|
||||||
|
SiteSetting.respond_to?(:assign_enabled) && SiteSetting.assign_enabled &&
|
||||||
|
(guardian.can_assign? || SiteSetting.assigns_public)
|
||||||
|
end
|
||||||
|
|
||||||
|
register_filter(/\Aassigned_to:(.+)\z/i) do |relation, name, filter|
|
||||||
|
if !assign_allowed?(filter.guardian)
|
||||||
|
raise Discourse::InvalidAccess.new(
|
||||||
|
"Assigns are not enabled or you do not have permission to see assigns.",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if (name == "nobody")
|
||||||
|
relation.joins("LEFT JOIN assignments a ON a.topic_id = topics.id AND a.active").where(
|
||||||
|
"a.assigned_to_id IS NULL",
|
||||||
|
)
|
||||||
|
elsif name == "*"
|
||||||
|
relation.joins("JOIN assignments a ON a.topic_id = topics.id AND a.active").where(
|
||||||
|
"a.assigned_to_id IS NOT NULL",
|
||||||
|
)
|
||||||
|
else
|
||||||
|
usernames = name.split(",").map(&:strip).map(&:downcase)
|
||||||
|
relation.joins("JOIN assignments a ON a.topic_id = topics.id AND a.active").where(
|
||||||
|
"a.assigned_to_id" => User.where(username_lower: usernames).select(:id),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
register_filter(/\Agroups?:([a-zA-Z0-9_\-,]+)\z/i) do |relation, groups_param, filter|
|
register_filter(/\Agroups?:([a-zA-Z0-9_\-,]+)\z/i) do |relation, groups_param, filter|
|
||||||
if groups_param.include?(",")
|
if groups_param.include?(",")
|
||||||
group_names = groups_param.split(",").map(&:strip)
|
group_names = groups_param.split(",").map(&:strip)
|
||||||
|
|
|
@ -15,13 +15,12 @@ RSpec.describe Jobs::StreamComposerHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "validates params" do
|
describe "validates params" do
|
||||||
let(:mode) { CompletionPrompt::PROOFREAD }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::PROOFREAD }
|
||||||
let(:prompt) { CompletionPrompt.find_by(id: mode) }
|
|
||||||
|
|
||||||
it "does nothing if there is no user" do
|
it "does nothing if there is no user" do
|
||||||
messages =
|
messages =
|
||||||
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion") do
|
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion") do
|
||||||
job.execute(user_id: nil, text: input, prompt: prompt.name, force_default_locale: false)
|
job.execute(user_id: nil, text: input, prompt: mode, force_default_locale: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(messages).to be_empty
|
expect(messages).to be_empty
|
||||||
|
@ -33,7 +32,7 @@ RSpec.describe Jobs::StreamComposerHelper do
|
||||||
job.execute(
|
job.execute(
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
text: nil,
|
text: nil,
|
||||||
prompt: prompt.name,
|
prompt: mode,
|
||||||
force_default_locale: false,
|
force_default_locale: false,
|
||||||
client_id: "123",
|
client_id: "123",
|
||||||
)
|
)
|
||||||
|
@ -44,12 +43,10 @@ RSpec.describe Jobs::StreamComposerHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when all params are provided" do
|
context "when all params are provided" do
|
||||||
let(:mode) { CompletionPrompt::PROOFREAD }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::PROOFREAD }
|
||||||
let(:prompt) { CompletionPrompt.find_by(id: mode) }
|
|
||||||
|
|
||||||
it "publishes updates with a partial result" do
|
it "publishes updates with a partial result" do
|
||||||
proofread_result = "I like to eat pie for breakfast because it is delicious."
|
proofread_result = "I like to eat pie for breakfast because it is delicious."
|
||||||
partial_result = "I"
|
|
||||||
|
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([proofread_result]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([proofread_result]) do
|
||||||
messages =
|
messages =
|
||||||
|
@ -57,7 +54,7 @@ RSpec.describe Jobs::StreamComposerHelper do
|
||||||
job.execute(
|
job.execute(
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
text: input,
|
text: input,
|
||||||
prompt: prompt.name,
|
prompt: mode,
|
||||||
force_default_locale: true,
|
force_default_locale: true,
|
||||||
client_id: "123",
|
client_id: "123",
|
||||||
)
|
)
|
||||||
|
@ -65,7 +62,7 @@ RSpec.describe Jobs::StreamComposerHelper do
|
||||||
|
|
||||||
partial_result_update = messages.first.data
|
partial_result_update = messages.first.data
|
||||||
expect(partial_result_update[:done]).to eq(false)
|
expect(partial_result_update[:done]).to eq(false)
|
||||||
expect(partial_result_update[:result]).to eq(partial_result)
|
expect(partial_result_update[:result]).to eq(proofread_result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -78,7 +75,7 @@ RSpec.describe Jobs::StreamComposerHelper do
|
||||||
job.execute(
|
job.execute(
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
text: input,
|
text: input,
|
||||||
prompt: prompt.name,
|
prompt: mode,
|
||||||
force_default_locale: true,
|
force_default_locale: true,
|
||||||
client_id: "123",
|
client_id: "123",
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,8 +23,7 @@ RSpec.describe Jobs::StreamPostHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "validates params" do
|
describe "validates params" do
|
||||||
let(:mode) { CompletionPrompt::EXPLAIN }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::EXPLAIN }
|
||||||
let(:prompt) { CompletionPrompt.find_by(id: mode) }
|
|
||||||
|
|
||||||
it "does nothing if there is no post" do
|
it "does nothing if there is no post" do
|
||||||
messages =
|
messages =
|
||||||
|
@ -55,24 +54,21 @@ RSpec.describe Jobs::StreamPostHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the prompt is explain" do
|
context "when the prompt is explain" do
|
||||||
let(:mode) { CompletionPrompt::EXPLAIN }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::EXPLAIN }
|
||||||
let(:prompt) { CompletionPrompt.find_by(id: mode) }
|
|
||||||
|
|
||||||
it "publishes updates with a partial result" do
|
it "publishes updates with a partial result" do
|
||||||
explanation =
|
explanation =
|
||||||
"In this context, \"pie\" refers to a baked dessert typically consisting of a pastry crust and filling."
|
"In this context, \"pie\" refers to a baked dessert typically consisting of a pastry crust and filling."
|
||||||
|
|
||||||
partial_explanation = "I"
|
|
||||||
|
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([explanation]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([explanation]) do
|
||||||
messages =
|
messages =
|
||||||
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
||||||
job.execute(post_id: post.id, user_id: user.id, text: "pie", prompt: prompt.name)
|
job.execute(post_id: post.id, user_id: user.id, text: "pie", prompt: mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
partial_result_update = messages.first.data
|
partial_result_update = messages.first.data
|
||||||
expect(partial_result_update[:done]).to eq(false)
|
expect(partial_result_update[:done]).to eq(false)
|
||||||
expect(partial_result_update[:result]).to eq(partial_explanation)
|
expect(partial_result_update[:result]).to eq(explanation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -83,7 +79,7 @@ RSpec.describe Jobs::StreamPostHelper do
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([explanation]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([explanation]) do
|
||||||
messages =
|
messages =
|
||||||
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
||||||
job.execute(post_id: post.id, user_id: user.id, text: "pie", prompt: prompt.name)
|
job.execute(post_id: post.id, user_id: user.id, text: "pie", prompt: mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
final_update = messages.last.data
|
final_update = messages.last.data
|
||||||
|
@ -94,23 +90,21 @@ RSpec.describe Jobs::StreamPostHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the prompt is translate" do
|
context "when the prompt is translate" do
|
||||||
let(:mode) { CompletionPrompt::TRANSLATE }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::TRANSLATE }
|
||||||
let(:prompt) { CompletionPrompt.find_by(id: mode) }
|
|
||||||
|
|
||||||
it "publishes updates with a partial result" do
|
it "publishes updates with a partial result" do
|
||||||
sentence = "I like to eat pie."
|
sentence = "I like to eat pie."
|
||||||
translation = "Me gusta comer pastel."
|
translation = "Me gusta comer pastel."
|
||||||
partial_translation = "M"
|
|
||||||
|
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([translation]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([translation]) do
|
||||||
messages =
|
messages =
|
||||||
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
||||||
job.execute(post_id: post.id, user_id: user.id, text: sentence, prompt: prompt.name)
|
job.execute(post_id: post.id, user_id: user.id, text: sentence, prompt: mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
partial_result_update = messages.first.data
|
partial_result_update = messages.first.data
|
||||||
expect(partial_result_update[:done]).to eq(false)
|
expect(partial_result_update[:done]).to eq(false)
|
||||||
expect(partial_result_update[:result]).to eq(partial_translation)
|
expect(partial_result_update[:result]).to eq(translation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -121,7 +115,7 @@ RSpec.describe Jobs::StreamPostHelper do
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([translation]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([translation]) do
|
||||||
messages =
|
messages =
|
||||||
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
MessageBus.track_publish("/discourse-ai/ai-helper/stream_suggestion/#{post.id}") do
|
||||||
job.execute(post_id: post.id, user_id: user.id, text: sentence, prompt: prompt.name)
|
job.execute(post_id: post.id, user_id: user.id, text: sentence, prompt: mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
final_update = messages.last.data
|
final_update = messages.last.data
|
||||||
|
|
|
@ -1,25 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Debugging https://github.com/ruby/net-protocol/issues/32
|
|
||||||
# which seems to be happening inconsistently in CI
|
|
||||||
Net::BufferedIO.prepend(
|
|
||||||
Module.new do
|
|
||||||
def initialize(*args, **kwargs)
|
|
||||||
if kwargs[:debug_output] && !kwargs[:debug_output].respond_to(:<<)
|
|
||||||
raise ArgumentError, "debug_output must support <<"
|
|
||||||
end
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def debug_output=(debug_output)
|
|
||||||
if debug_output && !debug_output.respond_to?(:<<)
|
|
||||||
raise ArgumentError, "debug_output must support <<"
|
|
||||||
end
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
)
|
|
||||||
|
|
||||||
describe DiscourseAi::Completions::CancelManager do
|
describe DiscourseAi::Completions::CancelManager do
|
||||||
fab!(:model) { Fabricate(:anthropic_model, name: "test-model") }
|
fab!(:model) { Fabricate(:anthropic_model, name: "test-model") }
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
RSpec.describe DiscourseAi::AiHelper::Assistant do
|
RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
fab!(:user)
|
fab!(:user)
|
||||||
fab!(:empty_locale_user) { Fabricate(:user, locale: "") }
|
fab!(:empty_locale_user) { Fabricate(:user, locale: "") }
|
||||||
let(:prompt) { CompletionPrompt.find_by(id: mode) }
|
|
||||||
|
|
||||||
before { assign_fake_provider_to(:ai_helper_model) }
|
before do
|
||||||
|
assign_fake_provider_to(:ai_helper_model)
|
||||||
|
Group.refresh_automatic_groups!
|
||||||
|
end
|
||||||
|
|
||||||
let(:english_text) { <<~STRING }
|
let(:english_text) { <<~STRING }
|
||||||
To perfect his horror, Caesar, surrounded at the base of the statue by the impatient daggers of his friends,
|
To perfect his horror, Caesar, surrounded at the base of the statue by the impatient daggers of his friends,
|
||||||
|
@ -48,15 +50,12 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
it "returns all available prompts" do
|
it "returns all available prompts" do
|
||||||
prompts = subject.available_prompts(user)
|
prompts = subject.available_prompts(user)
|
||||||
|
|
||||||
expect(prompts.length).to eq(8)
|
|
||||||
expect(prompts.map { |p| p[:name] }).to contain_exactly(
|
expect(prompts.map { |p| p[:name] }).to contain_exactly(
|
||||||
"translate",
|
"translate",
|
||||||
"generate_titles",
|
"generate_titles",
|
||||||
"proofread",
|
"proofread",
|
||||||
"markdown_table",
|
"markdown_table",
|
||||||
"custom_prompt",
|
|
||||||
"explain",
|
"explain",
|
||||||
"detect_text_locale",
|
|
||||||
"replace_dates",
|
"replace_dates",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -64,13 +63,12 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
it "returns all prompts to be shown in the composer" do
|
it "returns all prompts to be shown in the composer" do
|
||||||
prompts = subject.available_prompts(user)
|
prompts = subject.available_prompts(user)
|
||||||
filtered_prompts = prompts.select { |prompt| prompt[:location].include?("composer") }
|
filtered_prompts = prompts.select { |prompt| prompt[:location].include?("composer") }
|
||||||
expect(filtered_prompts.length).to eq(6)
|
|
||||||
expect(filtered_prompts.map { |p| p[:name] }).to contain_exactly(
|
expect(filtered_prompts.map { |p| p[:name] }).to contain_exactly(
|
||||||
"translate",
|
"translate",
|
||||||
"generate_titles",
|
"generate_titles",
|
||||||
"proofread",
|
"proofread",
|
||||||
"markdown_table",
|
"markdown_table",
|
||||||
"custom_prompt",
|
|
||||||
"replace_dates",
|
"replace_dates",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -78,12 +76,11 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
it "returns all prompts to be shown in the post menu" do
|
it "returns all prompts to be shown in the post menu" do
|
||||||
prompts = subject.available_prompts(user)
|
prompts = subject.available_prompts(user)
|
||||||
filtered_prompts = prompts.select { |prompt| prompt[:location].include?("post") }
|
filtered_prompts = prompts.select { |prompt| prompt[:location].include?("post") }
|
||||||
expect(filtered_prompts.length).to eq(4)
|
|
||||||
expect(filtered_prompts.map { |p| p[:name] }).to contain_exactly(
|
expect(filtered_prompts.map { |p| p[:name] }).to contain_exactly(
|
||||||
"translate",
|
"translate",
|
||||||
"explain",
|
"explain",
|
||||||
"proofread",
|
"proofread",
|
||||||
"custom_prompt",
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -101,90 +98,69 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
it "returns the illustrate_post prompt in the list of all prompts" do
|
it "returns the illustrate_post prompt in the list of all prompts" do
|
||||||
prompts = subject.available_prompts(user)
|
prompts = subject.available_prompts(user)
|
||||||
|
|
||||||
expect(prompts.length).to eq(9)
|
|
||||||
expect(prompts.map { |p| p[:name] }).to contain_exactly(
|
expect(prompts.map { |p| p[:name] }).to contain_exactly(
|
||||||
"translate",
|
"translate",
|
||||||
"generate_titles",
|
"generate_titles",
|
||||||
"proofread",
|
"proofread",
|
||||||
"markdown_table",
|
"markdown_table",
|
||||||
"custom_prompt",
|
|
||||||
"explain",
|
"explain",
|
||||||
"illustrate_post",
|
"illustrate_post",
|
||||||
"detect_text_locale",
|
|
||||||
"replace_dates",
|
"replace_dates",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe("#localize_prompt!") do
|
describe("#attach_user_context") do
|
||||||
before { SiteSetting.allow_user_locale = true }
|
before { SiteSetting.allow_user_locale = true }
|
||||||
|
|
||||||
|
let(:context) { DiscourseAi::Personas::BotContext.new(user: user) }
|
||||||
|
|
||||||
it "is able to perform %LANGUAGE% replacements" do
|
it "is able to perform %LANGUAGE% replacements" do
|
||||||
prompt =
|
subject.attach_user_context(context, user)
|
||||||
CompletionPrompt.new(messages: { insts: "This is a %LANGUAGE% test" }).messages_with_input(
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
|
|
||||||
subject.localize_prompt!(prompt, user)
|
expect(context.user_language).to eq("English (US)")
|
||||||
|
|
||||||
expect(prompt.messages[0][:content].strip).to eq("This is a English (US) test")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "handles users with empty string locales" do
|
it "handles users with empty string locales" do
|
||||||
prompt =
|
subject.attach_user_context(context, empty_locale_user)
|
||||||
CompletionPrompt.new(messages: { insts: "This is a %LANGUAGE% test" }).messages_with_input(
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
|
|
||||||
subject.localize_prompt!(prompt, empty_locale_user)
|
expect(context.user_language).to eq("English (US)")
|
||||||
|
|
||||||
expect(prompt.messages[0][:content].strip).to eq("This is a English (US) test")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with temporal context" do
|
context "with temporal context" do
|
||||||
let(:prompt) do
|
|
||||||
CompletionPrompt.new(
|
|
||||||
messages: {
|
|
||||||
insts: "Current context: {{temporal_context}}",
|
|
||||||
},
|
|
||||||
).messages_with_input("test")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "replaces temporal context with timezone information" do
|
it "replaces temporal context with timezone information" do
|
||||||
timezone = "America/New_York"
|
timezone = "America/New_York"
|
||||||
user.user_option.update!(timezone: timezone)
|
user.user_option.update!(timezone: timezone)
|
||||||
freeze_time "2024-01-01 12:00:00"
|
freeze_time "2024-01-01 12:00:00"
|
||||||
|
|
||||||
subject.localize_prompt!(prompt, user)
|
subject.attach_user_context(context, user)
|
||||||
|
|
||||||
content = prompt.messages[0][:content]
|
expect(context.temporal_context).to include(%("timezone":"America/New_York"))
|
||||||
|
|
||||||
expect(content).to include(%("timezone":"America/New_York"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "uses UTC as default timezone when user timezone is not set" do
|
it "uses UTC as default timezone when user timezone is not set" do
|
||||||
user.user_option.update!(timezone: nil)
|
user.user_option.update!(timezone: nil)
|
||||||
|
|
||||||
freeze_time "2024-01-01 12:00:00" do
|
freeze_time "2024-01-01 12:00:00" do
|
||||||
subject.localize_prompt!(prompt, user)
|
subject.attach_user_context(context, user)
|
||||||
|
|
||||||
parsed_context = JSON.parse(prompt.messages[0][:content].match(/context: (.+)$/)[1])
|
parsed_context = JSON.parse(context.temporal_context)
|
||||||
expect(parsed_context["user"]["timezone"]).to eq("UTC")
|
expect(parsed_context.dig("user", "timezone")).to eq("UTC")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not replace temporal context when user is nil" do
|
it "does not replace temporal context when user is nil" do
|
||||||
prompt_content = prompt.messages[0][:content].dup
|
subject.attach_user_context(context, nil)
|
||||||
subject.localize_prompt!(prompt, nil)
|
|
||||||
expect(prompt.messages[0][:content]).to eq(prompt_content)
|
expect(context.temporal_context).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#generate_and_send_prompt" do
|
describe "#generate_and_send_prompt" do
|
||||||
context "when using a prompt that returns text" do
|
context "when using a prompt that returns text" do
|
||||||
let(:mode) { CompletionPrompt::TRANSLATE }
|
let(:mode) { described_class::TRANSLATE }
|
||||||
|
|
||||||
let(:text_to_translate) { <<~STRING }
|
let(:text_to_translate) { <<~STRING }
|
||||||
Para que su horror sea perfecto, César, acosado al pie de la estatua por lo impacientes puñales de sus amigos,
|
Para que su horror sea perfecto, César, acosado al pie de la estatua por lo impacientes puñales de sus amigos,
|
||||||
|
@ -195,7 +171,7 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
it "Sends the prompt to the LLM and returns the response" do
|
it "Sends the prompt to the LLM and returns the response" do
|
||||||
response =
|
response =
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([english_text]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([english_text]) do
|
||||||
subject.generate_and_send_prompt(prompt, text_to_translate, user)
|
subject.generate_and_send_prompt(mode, text_to_translate, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(response[:suggestions]).to contain_exactly(english_text)
|
expect(response[:suggestions]).to contain_exactly(english_text)
|
||||||
|
@ -203,7 +179,7 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using a prompt that returns a list" do
|
context "when using a prompt that returns a list" do
|
||||||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
let(:mode) { described_class::GENERATE_TITLES }
|
||||||
|
|
||||||
let(:titles) do
|
let(:titles) do
|
||||||
"<item>The solitary horse</item><item>The horse etched in gold</item><item>A horse's infinite journey</item><item>A horse lost in time</item><item>A horse's last ride</item>"
|
"<item>The solitary horse</item><item>The horse etched in gold</item><item>A horse's infinite journey</item><item>A horse lost in time</item><item>A horse's last ride</item>"
|
||||||
|
@ -220,7 +196,7 @@ RSpec.describe DiscourseAi::AiHelper::Assistant do
|
||||||
|
|
||||||
response =
|
response =
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([titles]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([titles]) do
|
||||||
subject.generate_and_send_prompt(prompt, english_text, user)
|
subject.generate_and_send_prompt(mode, english_text, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(response[:suggestions]).to contain_exactly(*expected)
|
expect(response[:suggestions]).to contain_exactly(*expected)
|
||||||
|
|
|
@ -57,6 +57,8 @@ describe DiscourseAi::Utils::Research::Filter do
|
||||||
fab!(:bug_post) { Fabricate(:post, topic: bug_topic, user: user) }
|
fab!(:bug_post) { Fabricate(:post, topic: bug_topic, user: user) }
|
||||||
fab!(:feature_bug_post) { Fabricate(:post, topic: feature_bug_topic, user: user) }
|
fab!(:feature_bug_post) { Fabricate(:post, topic: feature_bug_topic, user: user) }
|
||||||
fab!(:no_tag_post) { Fabricate(:post, topic: no_tag_topic, user: user) }
|
fab!(:no_tag_post) { Fabricate(:post, topic: no_tag_topic, user: user) }
|
||||||
|
fab!(:admin1) { Fabricate(:admin, username: "admin1") }
|
||||||
|
fab!(:admin2) { Fabricate(:admin, username: "admin2") }
|
||||||
|
|
||||||
describe "group filtering" do
|
describe "group filtering" do
|
||||||
before do
|
before do
|
||||||
|
@ -192,6 +194,51 @@ describe DiscourseAi::Utils::Research::Filter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if SiteSetting.respond_to?(:assign_enabled)
|
||||||
|
describe "assign filtering" do
|
||||||
|
before do
|
||||||
|
SiteSetting.assign_enabled = true
|
||||||
|
assigner = Assigner.new(feature_topic, admin1)
|
||||||
|
assigner.assign(admin1)
|
||||||
|
|
||||||
|
assigner = Assigner.new(bug_topic, admin1)
|
||||||
|
assigner.assign(admin2)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:admin_guardian) { Guardian.new(admin1) }
|
||||||
|
|
||||||
|
it "can find topics assigned to a user" do
|
||||||
|
filter = described_class.new("assigned_to:#{admin1.username}", guardian: admin_guardian)
|
||||||
|
expect(filter.search.pluck(:id)).to contain_exactly(feature_post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can find topics assigned to multiple users" do
|
||||||
|
filter =
|
||||||
|
described_class.new(
|
||||||
|
"assigned_to:#{admin1.username},#{admin2.username}",
|
||||||
|
guardian: admin_guardian,
|
||||||
|
)
|
||||||
|
expect(filter.search.pluck(:id)).to contain_exactly(feature_post.id, bug_post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can find topics assigned to nobody" do
|
||||||
|
filter = described_class.new("assigned_to:nobody", guardian: admin_guardian)
|
||||||
|
expect(filter.search.pluck(:id)).to contain_exactly(feature_bug_post.id, no_tag_post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can find all assigned topics" do
|
||||||
|
filter = described_class.new("assigned_to:*", guardian: admin_guardian)
|
||||||
|
expect(filter.search.pluck(:id)).to contain_exactly(feature_post.id, bug_post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if assigns are disabled" do
|
||||||
|
SiteSetting.assign_enabled = false
|
||||||
|
filter = described_class.new("assigned_to:sam")
|
||||||
|
expect { filter.search }.to raise_error(Discourse::InvalidAccess)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "can limit number of results" do
|
it "can limit number of results" do
|
||||||
filter = described_class.new("category:Feedback max_results:1", limit: 5)
|
filter = described_class.new("category:Feedback max_results:1", limit: 5)
|
||||||
expect(filter.search.pluck(:id).length).to eq(1)
|
expect(filter.search.pluck(:id).length).to eq(1)
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.describe CompletionPrompt do
|
|
||||||
describe "validations" do
|
|
||||||
context "when there are too many messages" do
|
|
||||||
it "doesn't accept more than 20 messages" do
|
|
||||||
prompt = described_class.new(messages: [{ role: "system", content: "a" }] * 21)
|
|
||||||
|
|
||||||
expect(prompt.valid?).to eq(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when the message is over the max length" do
|
|
||||||
it "doesn't accept messages when the length is more than 1000 characters" do
|
|
||||||
prompt = described_class.new(messages: [{ role: "system", content: "a" * 1001 }])
|
|
||||||
|
|
||||||
expect(prompt.valid?).to eq(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "messages_with_input" do
|
|
||||||
let(:user_input) { "A user wrote this." }
|
|
||||||
|
|
||||||
context "when mapping to a prompt" do
|
|
||||||
it "correctly maps everything to the prompt" do
|
|
||||||
cp =
|
|
||||||
CompletionPrompt.new(
|
|
||||||
messages: {
|
|
||||||
insts: "Instructions",
|
|
||||||
post_insts: "Post Instructions",
|
|
||||||
examples: [["Request 1", "Response 1"]],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt = cp.messages_with_input("hello")
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
{ type: :system, content: "Instructions\nPost Instructions" },
|
|
||||||
{ type: :user, content: "Request 1" },
|
|
||||||
{ type: :model, content: "Response 1" },
|
|
||||||
{ type: :user, content: "<input>hello</input>" },
|
|
||||||
]
|
|
||||||
|
|
||||||
expect(prompt.messages).to eq(expected)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when the record has the custom_prompt type" do
|
|
||||||
let(:custom_prompt) { described_class.find(described_class::CUSTOM_PROMPT) }
|
|
||||||
|
|
||||||
it "wraps the user input with <input> XML tags and adds a custom instruction if given" do
|
|
||||||
expected = <<~TEXT.strip
|
|
||||||
<input>Translate to Turkish:
|
|
||||||
#{user_input}</input>
|
|
||||||
TEXT
|
|
||||||
|
|
||||||
custom_prompt.custom_instruction = "Translate to Turkish"
|
|
||||||
|
|
||||||
prompt = custom_prompt.messages_with_input(user_input)
|
|
||||||
|
|
||||||
expect(prompt.messages.last[:content]).to eq(expected)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when the records don't have the custom_prompt type" do
|
|
||||||
let(:title_prompt) { described_class.find(described_class::GENERATE_TITLES) }
|
|
||||||
|
|
||||||
it "wraps user input with <input> XML tags" do
|
|
||||||
expected = "<input>#{user_input}</input>"
|
|
||||||
|
|
||||||
title_prompt.custom_instruction = "Translate to Turkish"
|
|
||||||
|
|
||||||
prompt = title_prompt.messages_with_input(user_input)
|
|
||||||
|
|
||||||
expect(prompt.messages.last[:content]).to eq(expected)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -11,6 +11,7 @@ describe Plugin::Instance do
|
||||||
SiteSetting.ai_helper_enabled = true
|
SiteSetting.ai_helper_enabled = true
|
||||||
SiteSetting.ai_helper_illustrate_post_model = "disabled"
|
SiteSetting.ai_helper_illustrate_post_model = "disabled"
|
||||||
Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user)
|
Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user)
|
||||||
|
Group.refresh_automatic_groups!
|
||||||
|
|
||||||
DiscourseAi::AiHelper::Assistant.clear_prompt_cache!
|
DiscourseAi::AiHelper::Assistant.clear_prompt_cache!
|
||||||
end
|
end
|
||||||
|
@ -19,7 +20,15 @@ describe Plugin::Instance do
|
||||||
|
|
||||||
it "returns the available prompts" do
|
it "returns the available prompts" do
|
||||||
expect(serializer.ai_helper_prompts).to be_present
|
expect(serializer.ai_helper_prompts).to be_present
|
||||||
expect(serializer.ai_helper_prompts.object.count).to eq(8)
|
|
||||||
|
expect(serializer.ai_helper_prompts.object.map { |p| p[:name] }).to contain_exactly(
|
||||||
|
"translate",
|
||||||
|
"generate_titles",
|
||||||
|
"proofread",
|
||||||
|
"markdown_table",
|
||||||
|
"explain",
|
||||||
|
"replace_dates",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe DiscourseAi::AiHelper::AssistantController do
|
||||||
text: "hello wrld",
|
text: "hello wrld",
|
||||||
location: "composer",
|
location: "composer",
|
||||||
client_id: "1234",
|
client_id: "1234",
|
||||||
mode: CompletionPrompt::PROOFREAD,
|
mode: DiscourseAi::AiHelper::Assistant::PROOFREAD,
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
|
@ -41,7 +41,7 @@ RSpec.describe DiscourseAi::AiHelper::AssistantController do
|
||||||
describe "#suggest" do
|
describe "#suggest" do
|
||||||
let(:text_to_proofread) { "The rain in spain stays mainly in the plane." }
|
let(:text_to_proofread) { "The rain in spain stays mainly in the plane." }
|
||||||
let(:proofread_text) { "The rain in Spain, stays mainly in the Plane." }
|
let(:proofread_text) { "The rain in Spain, stays mainly in the Plane." }
|
||||||
let(:mode) { CompletionPrompt::PROOFREAD }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::PROOFREAD }
|
||||||
|
|
||||||
context "when not logged in" do
|
context "when not logged in" do
|
||||||
it "returns a 403 response" do
|
it "returns a 403 response" do
|
||||||
|
@ -121,7 +121,7 @@ RSpec.describe DiscourseAi::AiHelper::AssistantController do
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([translated_text]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([translated_text]) do
|
||||||
post "/discourse-ai/ai-helper/suggest",
|
post "/discourse-ai/ai-helper/suggest",
|
||||||
params: {
|
params: {
|
||||||
mode: CompletionPrompt::CUSTOM_PROMPT,
|
mode: DiscourseAi::AiHelper::Assistant::CUSTOM_PROMPT,
|
||||||
text: "A user wrote this",
|
text: "A user wrote this",
|
||||||
custom_prompt: "Translate to Spanish",
|
custom_prompt: "Translate to Spanish",
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ RSpec.describe DiscourseAi::AiHelper::AssistantController do
|
||||||
expect {
|
expect {
|
||||||
post "/discourse-ai/ai-helper/suggest",
|
post "/discourse-ai/ai-helper/suggest",
|
||||||
params: {
|
params: {
|
||||||
mode: CompletionPrompt::ILLUSTRATE_POST,
|
mode: DiscourseAi::AiHelper::Assistant::ILLUSTRATE_POST,
|
||||||
text: text_to_proofread,
|
text: text_to_proofread,
|
||||||
force_default_locale: true,
|
force_default_locale: true,
|
||||||
}
|
}
|
||||||
|
@ -153,8 +153,14 @@ RSpec.describe DiscourseAi::AiHelper::AssistantController do
|
||||||
amount = rate_limit[:amount]
|
amount = rate_limit[:amount]
|
||||||
|
|
||||||
amount.times do
|
amount.times do
|
||||||
post "/discourse-ai/ai-helper/suggest", params: { mode: mode, text: text_to_proofread }
|
DiscourseAi::Completions::Llm.with_prepared_responses([proofread_text]) do
|
||||||
expect(response.status).to eq(200)
|
post "/discourse-ai/ai-helper/suggest",
|
||||||
|
params: {
|
||||||
|
mode: mode,
|
||||||
|
text: text_to_proofread,
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([proofread_text]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([proofread_text]) do
|
||||||
post "/discourse-ai/ai-helper/suggest", params: { mode: mode, text: text_to_proofread }
|
post "/discourse-ai/ai-helper/suggest", params: { mode: mode, text: text_to_proofread }
|
||||||
|
|
|
@ -51,6 +51,15 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
|
||||||
|
|
||||||
expected_tools = [["Read", { "read_private" => nil }, true], ["ListCategories", {}, true]]
|
expected_tools = [["Read", { "read_private" => nil }, true], ["ListCategories", {}, true]]
|
||||||
expect(persona.tools).to contain_exactly(*expected_tools)
|
expect(persona.tools).to contain_exactly(*expected_tools)
|
||||||
|
|
||||||
|
# lets also test upgrades here... particularly one options was deleted and another added
|
||||||
|
# this ensurse that we can still edit the tool correctly and all options are present
|
||||||
|
persona.update!(tools: [["Read", { "got_deleted" => true }]])
|
||||||
|
|
||||||
|
visit "/admin/plugins/discourse-ai/ai-personas/#{persona_id}/edit"
|
||||||
|
|
||||||
|
expect(page).to have_selector("input[name='toolOptions.Read.read_private']")
|
||||||
|
expect(page).not_to have_selector("input[name='toolOptions.Read.got_deleted']")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "will not allow deletion or editing of system personas" do
|
it "will not allow deletion or editing of system personas" do
|
||||||
|
|
|
@ -62,7 +62,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using custom prompt" do
|
context "when using custom prompt" do
|
||||||
let(:mode) { CompletionPrompt::CUSTOM_PROMPT }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::CUSTOM_PROMPT }
|
||||||
|
|
||||||
let(:custom_prompt_input) { "Translate to French" }
|
let(:custom_prompt_input) { "Translate to French" }
|
||||||
let(:custom_prompt_response) { "La pluie en Espagne reste principalement dans l'avion." }
|
let(:custom_prompt_response) { "La pluie en Espagne reste principalement dans l'avion." }
|
||||||
|
@ -94,7 +94,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when not a member of custom prompt group" do
|
context "when not a member of custom prompt group" do
|
||||||
let(:mode) { CompletionPrompt::CUSTOM_PROMPT }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::CUSTOM_PROMPT }
|
||||||
before { SiteSetting.ai_helper_custom_prompts_allowed_groups = non_member_group.id.to_s }
|
before { SiteSetting.ai_helper_custom_prompts_allowed_groups = non_member_group.id.to_s }
|
||||||
|
|
||||||
it "does not show custom prompt option" do
|
it "does not show custom prompt option" do
|
||||||
|
@ -104,7 +104,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using translation mode" do
|
context "when using translation mode" do
|
||||||
let(:mode) { CompletionPrompt::TRANSLATE }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::TRANSLATE }
|
||||||
|
|
||||||
let(:spanish_input) { "La lluvia en España se queda principalmente en el avión." }
|
let(:spanish_input) { "La lluvia en España se queda principalmente en el avión." }
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using the proofreading mode" do
|
context "when using the proofreading mode" do
|
||||||
let(:mode) { CompletionPrompt::PROOFREAD }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::PROOFREAD }
|
||||||
|
|
||||||
let(:proofread_text) { "The rain in Spain, stays mainly in the Plane." }
|
let(:proofread_text) { "The rain in Spain, stays mainly in the Plane." }
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when suggesting titles with AI title suggester" do
|
context "when suggesting titles with AI title suggester" do
|
||||||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::GENERATE_TITLES }
|
||||||
|
|
||||||
let(:titles) do
|
let(:titles) do
|
||||||
"<item>Rainy Spain</item><item>Plane-Bound Delights</item><item>Mysterious Spain</item><item>Plane-Rain Chronicles</item><item>Unveiling Spain</item>"
|
"<item>Rainy Spain</item><item>Plane-Bound Delights</item><item>Mysterious Spain</item><item>Plane-Rain Chronicles</item><item>Unveiling Spain</item>"
|
||||||
|
@ -330,7 +330,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when AI helper is disabled" do
|
context "when AI helper is disabled" do
|
||||||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::GENERATE_TITLES }
|
||||||
before { SiteSetting.ai_helper_enabled = false }
|
before { SiteSetting.ai_helper_enabled = false }
|
||||||
|
|
||||||
it "does not show the AI helper button in the composer toolbar" do
|
it "does not show the AI helper button in the composer toolbar" do
|
||||||
|
@ -349,7 +349,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when user is not a member of AI helper allowed group" do
|
context "when user is not a member of AI helper allowed group" do
|
||||||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::GENERATE_TITLES }
|
||||||
before { SiteSetting.composer_ai_helper_allowed_groups = non_member_group.id.to_s }
|
before { SiteSetting.composer_ai_helper_allowed_groups = non_member_group.id.to_s }
|
||||||
|
|
||||||
it "does not show the AI helper button in the composer toolbar" do
|
it "does not show the AI helper button in the composer toolbar" do
|
||||||
|
@ -368,7 +368,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when suggestion features are disabled" do
|
context "when suggestion features are disabled" do
|
||||||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::GENERATE_TITLES }
|
||||||
before { SiteSetting.ai_helper_enabled_features = "context_menu" }
|
before { SiteSetting.ai_helper_enabled_features = "context_menu" }
|
||||||
|
|
||||||
it "does not show suggestion buttons in the composer" do
|
it "does not show suggestion buttons in the composer" do
|
||||||
|
@ -398,7 +398,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
||||||
composer.click_toolbar_button("ai-helper-trigger")
|
composer.click_toolbar_button("ai-helper-trigger")
|
||||||
|
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
|
||||||
ai_helper_menu.select_helper_model(CompletionPrompt::TRANSLATE)
|
ai_helper_menu.select_helper_model(DiscourseAi::AiHelper::Assistant::TRANSLATE)
|
||||||
expect(ai_helper_menu).to have_no_context_menu
|
expect(ai_helper_menu).to have_no_context_menu
|
||||||
expect(diff_modal).to be_visible
|
expect(diff_modal).to be_visible
|
||||||
end
|
end
|
||||||
|
|
|
@ -80,7 +80,7 @@ RSpec.describe "AI Post helper", type: :system, js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using proofread mode" do
|
context "when using proofread mode" do
|
||||||
let(:mode) { CompletionPrompt::PROOFREAD }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::PROOFREAD }
|
||||||
let(:proofread_response) do
|
let(:proofread_response) do
|
||||||
"The Toyota Supra delivers 382 horsepower making it a very fast car."
|
"The Toyota Supra delivers 382 horsepower making it a very fast car."
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,7 +54,7 @@ RSpec.describe "AI Post helper", type: :system, js: true do
|
||||||
|
|
||||||
describe "moving posts to a new topic" do
|
describe "moving posts to a new topic" do
|
||||||
context "when suggesting titles with AI title suggester" do
|
context "when suggesting titles with AI title suggester" do
|
||||||
let(:mode) { CompletionPrompt::GENERATE_TITLES }
|
let(:mode) { DiscourseAi::AiHelper::Assistant::GENERATE_TITLES }
|
||||||
let(:titles) do
|
let(:titles) do
|
||||||
"<item>Pie: A delicious dessert</item><item>Cake is the best!</item><item>Croissants are delightful</item><item>Some great desserts</item><item>What is the best dessert?</item>"
|
"<item>Pie: A delicious dessert</item><item>Cake is the best!</item><item>Croissants are delightful</item><item>Some great desserts</item><item>What is the best dessert?</item>"
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@ module PageObjects
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_helper_model(mode)
|
def select_helper_model(mode)
|
||||||
find("#{OPTIONS_SELECTOR} .btn[data-value=\"#{mode}\"]").click
|
find("#{OPTIONS_SELECTOR} .btn[data-name=\"#{mode}\"]").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def suggestion_value
|
def suggestion_value
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
id: -301,
|
|
||||||
name: "translate",
|
name: "translate",
|
||||||
translated_name: "Translate to English (US)",
|
translated_name: "Translate to English (US)",
|
||||||
prompt_type: "text",
|
prompt_type: "text",
|
||||||
|
@ -8,7 +7,6 @@ export default [
|
||||||
location: ["composer", "post"],
|
location: ["composer", "post"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: -303,
|
|
||||||
name: "proofread",
|
name: "proofread",
|
||||||
translated_name: "Proofread text",
|
translated_name: "Proofread text",
|
||||||
prompt_type: "diff",
|
prompt_type: "diff",
|
||||||
|
@ -16,7 +14,6 @@ export default [
|
||||||
location: ["composer", "post"],
|
location: ["composer", "post"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: -304,
|
|
||||||
name: "markdown_table",
|
name: "markdown_table",
|
||||||
translated_name: "Generate Markdown table",
|
translated_name: "Generate Markdown table",
|
||||||
prompt_type: "diff",
|
prompt_type: "diff",
|
||||||
|
@ -24,7 +21,6 @@ export default [
|
||||||
location: ["composer"],
|
location: ["composer"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: -305,
|
|
||||||
name: "custom_prompt",
|
name: "custom_prompt",
|
||||||
translated_name: "Custom Prompt",
|
translated_name: "Custom Prompt",
|
||||||
prompt_type: "diff",
|
prompt_type: "diff",
|
||||||
|
@ -32,7 +28,6 @@ export default [
|
||||||
location: ["composer", "post"],
|
location: ["composer", "post"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: -306,
|
|
||||||
name: "explain",
|
name: "explain",
|
||||||
translated_name: "Explain",
|
translated_name: "Explain",
|
||||||
prompt_type: "text",
|
prompt_type: "text",
|
||||||
|
@ -40,7 +35,6 @@ export default [
|
||||||
location: ["post"],
|
location: ["post"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: -307,
|
|
||||||
name: "generate_titles",
|
name: "generate_titles",
|
||||||
translated_name: "Suggest topic titles",
|
translated_name: "Suggest topic titles",
|
||||||
prompt_type: "list",
|
prompt_type: "list",
|
||||||
|
@ -48,19 +42,10 @@ export default [
|
||||||
location: ["composer"],
|
location: ["composer"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: -308,
|
|
||||||
name: "illustrate_post",
|
name: "illustrate_post",
|
||||||
translated_name: "Illustrate Post",
|
translated_name: "Illustrate Post",
|
||||||
prompt_type: "list",
|
prompt_type: "list",
|
||||||
icon: "images",
|
icon: "images",
|
||||||
location: ["composer"],
|
location: ["composer"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: -309,
|
|
||||||
name: "detect_text_locale",
|
|
||||||
translated_name: "detect_text_locale",
|
|
||||||
prompt_type: "text",
|
|
||||||
icon: null,
|
|
||||||
location: [],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue