Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 992ae04

Browse files
committed
FEATURE: Examples support for personas.
Examples simulate previous interactions with an LLM and come right after the system prompt. This helps grounding the model and producing better responses.
1 parent cf45e68 commit 992ae04

File tree

16 files changed

+295
-90
lines changed

16 files changed

+295
-90
lines changed

app/controllers/discourse_ai/admin/ai_personas_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ def ai_persona_params
225225
permitted[:response_format] = permit_response_format(response_format)
226226
end
227227

228+
if examples = params.dig(:ai_persona, :examples)
229+
permitted[:examples] = permit_examples(examples)
230+
end
231+
228232
permitted
229233
end
230234

@@ -251,6 +255,12 @@ def permit_response_format(response_format)
251255
end
252256
end
253257
end
258+
259+
def permit_examples(examples)
260+
return [] if !examples.is_a?(Array)
261+
262+
examples.map { |example_arr| example_arr.take(2).map(&:to_s) }
263+
end
254264
end
255265
end
256266
end

app/models/ai_persona.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class AiPersona < ActiveRecord::Base
1313
validate :system_persona_unchangeable, on: :update, if: :system
1414
validate :chat_preconditions
1515
validate :allowed_seeded_model, if: :default_llm_id
16+
validate :well_formated_examples
1617
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
1718
# leaves some room for growth but sets a maximum to avoid memory issues
1819
# we may want to revisit this in the future
@@ -265,6 +266,7 @@ def class_instance
265266
define_method(:top_p) { @ai_persona&.top_p }
266267
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
267268
define_method(:uploads) { @ai_persona&.uploads }
269+
define_method(:examples) { @ai_persona&.examples }
268270
end
269271
end
270272

@@ -343,6 +345,11 @@ def system_persona_unchangeable
343345
new_format = response_format_change[1].map { |f| f["key"] }.to_set
344346

345347
errors.add(:base, error_msg) if old_format != new_format
348+
elsif examples_changed?
349+
old_examples = examples_change[0].flatten.to_set
350+
new_examples = examples_change[1].flatten.to_set
351+
352+
errors.add(:base, error_msg) if old_examples != new_examples
346353
end
347354
end
348355

@@ -363,6 +370,17 @@ def allowed_seeded_model
363370

364371
errors.add(:default_llm, I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"))
365372
end
373+
374+
def well_formated_examples
375+
return if examples.blank?
376+
377+
if examples.is_a?(Array) &&
378+
examples.all? { |e| e.is_a?(Array) && e.length == 2 && e.all?(&:present?) }
379+
return
380+
end
381+
382+
errors.add(:examples, I18n.t("discourse_ai.personas.malformed_examples"))
383+
end
366384
end
367385

368386
# == Schema Information
@@ -401,6 +419,7 @@ def allowed_seeded_model
401419
# default_llm_id :bigint
402420
# question_consolidator_llm_id :bigint
403421
# response_format :jsonb
422+
# examples :jsonb
404423
#
405424
# Indexes
406425
#

app/serializers/localized_ai_persona_serializer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer
3131
:allow_topic_mentions,
3232
:allow_personal_messages,
3333
:force_default_llm,
34-
:response_format
34+
:response_format,
35+
:examples
3536

3637
has_one :user, serializer: BasicUserSerializer, embed: :object
3738
has_many :rag_uploads, serializer: UploadSerializer, embed: :object

assets/javascripts/discourse/admin/models/ai-persona.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const CREATE_ATTRIBUTES = [
3434
"allow_chat_channel_mentions",
3535
"allow_chat_direct_messages",
3636
"response_format",
37+
"examples",
3738
];
3839

3940
const SYSTEM_ATTRIBUTES = [
@@ -61,7 +62,6 @@ const SYSTEM_ATTRIBUTES = [
6162
"allow_topic_mentions",
6263
"allow_chat_channel_mentions",
6364
"allow_chat_direct_messages",
64-
"response_format",
6565
];
6666

6767
export default class AiPersona extends RestModel {
@@ -154,6 +154,7 @@ export default class AiPersona extends RestModel {
154154
this.populateTools(attrs);
155155
attrs.forced_tool_count = this.forced_tool_count || -1;
156156
attrs.response_format = attrs.response_format || [];
157+
attrs.examples = attrs.examples || [];
157158

158159
return attrs;
159160
}

assets/javascripts/discourse/components/ai-persona-editor.gjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import AdminUser from "admin/models/admin-user";
1717
import GroupChooser from "select-kit/components/group-chooser";
1818
import AiPersonaResponseFormatEditor from "../components/modal/ai-persona-response-format-editor";
1919
import AiLlmSelector from "./ai-llm-selector";
20+
import AiPersonaCollapsableExample from "./ai-persona-example";
2021
import AiPersonaToolOptions from "./ai-persona-tool-options";
2122
import AiToolSelector from "./ai-tool-selector";
2223
import RagOptionsFk from "./rag-options-fk";
@@ -230,6 +231,12 @@ export default class PersonaEditor extends Component {
230231
return this.allTools.filter((tool) => tools.includes(tool.id));
231232
}
232233

234+
@action
235+
addExamplesPair(form, data) {
236+
const newExamples = [...data.examples, ["", ""]];
237+
form.set("examples", newExamples);
238+
}
239+
233240
mapToolOptions(currentOptions, toolNames) {
234241
const updatedOptions = Object.assign({}, currentOptions);
235242

@@ -422,6 +429,32 @@ export default class PersonaEditor extends Component {
422429
</form.Field>
423430
{{/unless}}
424431

432+
<form.Section
433+
@title={{i18n "discourse_ai.ai_persona.examples.title"}}
434+
@subtitle={{i18n "discourse_ai.ai_persona.examples.examples_help"}}
435+
>
436+
{{#unless data.system}}
437+
<form.Container>
438+
<form.Button
439+
@action={{fn this.addExamplesPair form data}}
440+
@label="discourse_ai.ai_persona.examples.new"
441+
class="ai-persona-editor__new_example"
442+
/>
443+
</form.Container>
444+
{{/unless}}
445+
446+
{{#if (gt data.examples.length 0)}}
447+
<form.Collection @name="examples" as |exCollection exCollectionIdx|>
448+
<AiPersonaCollapsableExample
449+
@examplesCollection={{exCollection}}
450+
@exampleNumber={{exCollectionIdx}}
451+
@system={{data.system}}
452+
@form={{form}}
453+
/>
454+
</form.Collection>
455+
{{/if}}
456+
</form.Section>
457+
425458
<form.Section @title={{i18n "discourse_ai.ai_persona.ai_tools"}}>
426459
<form.Field
427460
@name="tools"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { concat } from "@ember/helper";
4+
import { on } from "@ember/modifier";
5+
import { action } from "@ember/object";
6+
import { eq } from "truth-helpers";
7+
import icon from "discourse/helpers/d-icon";
8+
import { i18n } from "discourse-i18n";
9+
10+
export default class AiPersonaCollapsableExample extends Component {
11+
@tracked collapsed = true;
12+
13+
get caretIcon() {
14+
return this.collapsed ? "angle-right" : "angle-down";
15+
}
16+
17+
@action
18+
toggleExample() {
19+
this.collapsed = !this.collapsed;
20+
}
21+
22+
@action
23+
deletePair() {
24+
this.collapsed = true;
25+
this.args.examplesCollection.remove(this.args.exampleNumber);
26+
}
27+
28+
get exampleTitle() {
29+
return i18n("discourse_ai.ai_persona.examples.collapsable_title", {
30+
number: this.args.exampleNumber + 1,
31+
});
32+
}
33+
34+
<template>
35+
<div role="button" {{on "click" this.toggleExample}}>
36+
<span>{{icon this.caretIcon}}</span>
37+
{{this.exampleTitle}}
38+
</div>
39+
{{#unless this.collapsed}}
40+
<@examplesCollection.Collection as |exPair pairIdx|>
41+
<exPair.Field
42+
@title={{i18n
43+
(concat
44+
"discourse_ai.ai_persona.examples."
45+
(if (eq pairIdx 0) "user" "model")
46+
)
47+
}}
48+
@validation="required|length:1,100"
49+
@disabled={{@system}}
50+
as |field|
51+
>
52+
<field.Textarea />
53+
</exPair.Field>
54+
</@examplesCollection.Collection>
55+
56+
{{#unless @system}}
57+
<@form.Container>
58+
<@form.Button
59+
@action={{this.deletePair}}
60+
@label="discourse_ai.ai_persona.examples.remove"
61+
class="ai-persona-editor__delete_example btn-danger"
62+
/>
63+
</@form.Container>
64+
{{/unless}}
65+
{{/unless}}
66+
</template>
67+
}

config/locales/client.en.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,14 @@ en:
330330
modal:
331331
root_title: "Response structure"
332332
key_title: "Key"
333+
examples:
334+
title: Examples
335+
examples_help: Simulate previous interactions with the LLM and ground it to produce better result.
336+
new: New example
337+
remove: Delete example
338+
collapsable_title: "Example #%{number}"
339+
user: "User message"
340+
model: "Model response"
333341

334342
list:
335343
enabled: "AI Bot?"

config/locales/server.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ en:
495495
other: "We couldn't delete this model because %{settings} are using it. Update the settings and try again."
496496
cannot_edit_builtin: "You can't edit a built-in model."
497497

498+
personas:
499+
malformed_examples: "The given examples have the wrong format."
500+
498501
embeddings:
499502
delete_failed: "This model is currently in use. Update the `ai embeddings selected model` first."
500503
cannot_edit_builtin: "You can't edit a built-in model."

db/fixtures/personas/603_ai_personas.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def from_setting(setting_name)
7474

7575
persona.response_format = instance.response_format
7676

77+
persona.examples = instance.examples
78+
7779
persona.system_prompt = instance.system_prompt
7880
persona.top_p = instance.top_p
7981
persona.temperature = instance.temperature
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class AddExamplesToPersonas < ActiveRecord::Migration[7.2]
4+
def change
5+
add_column :ai_personas, :examples, :jsonb
6+
end
7+
end

0 commit comments

Comments
 (0)