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

Commit c364743

Browse files
committed
Response format can be specified in the persona
1 parent e56e93c commit c364743

File tree

20 files changed

+285
-56
lines changed

20 files changed

+285
-56
lines changed

app/controllers/discourse_ai/admin/ai_personas_controller.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ def ai_persona_params
221221
permitted[:tools] = permit_tools(tools)
222222
end
223223

224+
if response_format = params.dig(:ai_persona, :response_format)
225+
permitted[:response_format] = permit_response_format(response_format)
226+
end
227+
224228
permitted
225229
end
226230

@@ -235,6 +239,18 @@ def permit_tools(tools)
235239
[tool, options, !!force_tool]
236240
end
237241
end
242+
243+
def permit_response_format(response_format)
244+
return [] if !response_format.is_a?(Array)
245+
246+
response_format.map do |element|
247+
if element && element.is_a?(ActionController::Parameters)
248+
element.permit!
249+
else
250+
false
251+
end
252+
end
253+
end
238254
end
239255
end
240256
end

app/models/ai_persona.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,19 +318,24 @@ def chat_preconditions
318318
end
319319

320320
def system_persona_unchangeable
321+
error_msg = I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona")
322+
321323
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
322324
description_changed?
323-
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
325+
errors.add(:base, error_msg)
324326
elsif tools_changed?
325327
old_tools = tools_change[0]
326328
new_tools = tools_change[1]
327329

328330
old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
329331
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
330332

331-
if old_tool_names != new_tool_names
332-
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
333-
end
333+
errors.add(:base, error_msg) if old_tool_names != new_tool_names
334+
elsif response_format_changed?
335+
old_format = response_format_change[0].map { |f| f["key"] }.to_set
336+
new_format = response_format_change[1].map { |f| f["key"] }.to_set
337+
338+
errors.add(:base, error_msg) if old_format != new_format
334339
end
335340
end
336341

@@ -388,6 +393,7 @@ def allowed_seeded_model
388393
# rag_llm_model_id :bigint
389394
# default_llm_id :bigint
390395
# question_consolidator_llm_id :bigint
396+
# response_format :json not null
391397
#
392398
# Indexes
393399
#

app/serializers/localized_ai_persona_serializer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer
3030
:allow_chat_direct_messages,
3131
:allow_topic_mentions,
3232
:allow_personal_messages,
33-
:force_default_llm
33+
:force_default_llm,
34+
:response_format
3435

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const CREATE_ATTRIBUTES = [
3333
"allow_topic_mentions",
3434
"allow_chat_channel_mentions",
3535
"allow_chat_direct_messages",
36+
"response_format",
3637
];
3738

3839
const SYSTEM_ATTRIBUTES = [
@@ -60,6 +61,7 @@ const SYSTEM_ATTRIBUTES = [
6061
"allow_topic_mentions",
6162
"allow_chat_channel_mentions",
6263
"allow_chat_direct_messages",
64+
"response_format",
6365
];
6466

6567
export default class AiPersona extends RestModel {
@@ -151,6 +153,7 @@ export default class AiPersona extends RestModel {
151153
const attrs = this.getProperties(CREATE_ATTRIBUTES);
152154
this.populateTools(attrs);
153155
attrs.forced_tool_count = this.forced_tool_count || -1;
156+
attrs.response_format = attrs.response_format || [];
154157

155158
return attrs;
156159
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Group from "discourse/models/group";
1515
import { i18n } from "discourse-i18n";
1616
import AdminUser from "admin/models/admin-user";
1717
import GroupChooser from "select-kit/components/group-chooser";
18+
import AiPersonaResponseFormatEditor from "../components/modal/ai-persona-response-format-editor";
1819
import AiLlmSelector from "./ai-llm-selector";
1920
import AiPersonaToolOptions from "./ai-persona-tool-options";
2021
import AiToolSelector from "./ai-tool-selector";
@@ -325,6 +326,8 @@ export default class PersonaEditor extends Component {
325326
<field.Textarea />
326327
</form.Field>
327328

329+
<AiPersonaResponseFormatEditor @form={{form}} @data={{data}} />
330+
328331
<form.Field
329332
@name="default_llm_id"
330333
@title={{i18n "discourse_ai.ai_persona.default_llm"}}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { fn, hash } from "@ember/helper";
4+
import { action } from "@ember/object";
5+
import { gt } from "truth-helpers";
6+
import ModalJsonSchemaEditor from "discourse/components/modal/json-schema-editor";
7+
import { prettyJSON } from "discourse/lib/formatter";
8+
import { i18n } from "discourse-i18n";
9+
10+
export default class AiPersonaResponseFormatEditor extends Component {
11+
@tracked showJsonEditorModal = false;
12+
13+
jsonSchema = {
14+
type: "array",
15+
uniqueItems: true,
16+
title: i18n("discourse_ai.ai_persona.response_format.modal.root_title"),
17+
items: {
18+
type: "object",
19+
title: i18n("discourse_ai.ai_persona.response_format.modal.key_title"),
20+
properties: {
21+
key: {
22+
type: "string",
23+
},
24+
type: {
25+
type: "string",
26+
enum: ["string", "integer", "boolean"],
27+
},
28+
},
29+
},
30+
};
31+
32+
get editorTitle() {
33+
return i18n("discourse_ai.ai_persona.response_format.title");
34+
}
35+
36+
get responseFormatAsJSON() {
37+
return JSON.stringify(this.args.data.response_format);
38+
}
39+
40+
get displayJSON() {
41+
const toDisplay = {};
42+
43+
this.args.data.response_format.forEach((keyDesc) => {
44+
toDisplay[keyDesc.key] = keyDesc.type;
45+
});
46+
47+
return prettyJSON(toDisplay);
48+
}
49+
50+
@action
51+
openModal() {
52+
this.showJsonEditorModal = true;
53+
}
54+
55+
@action
56+
closeModal() {
57+
this.showJsonEditorModal = false;
58+
}
59+
60+
@action
61+
updateResponseFormat(form, value) {
62+
form.set("response_format", JSON.parse(value));
63+
}
64+
65+
<template>
66+
<@form.Container @title={{this.editorTitle}} @format="large">
67+
<div class="ai-persona-editor__response-format">
68+
{{#if (gt @data.response_format.length 0)}}
69+
<pre class="ai-persona-editor__response-format-pre">
70+
<code>{{this.displayJSON}}</code>
71+
</pre>
72+
{{else}}
73+
<div class="ai-persona-editor__response-format-none">{{i18n
74+
"discourse_ai.ai_persona.response_format.no_format"
75+
}}</div>
76+
{{/if}}
77+
78+
<@form.Button
79+
@action={{this.openModal}}
80+
@label="discourse_ai.ai_persona.response_format.open_modal"
81+
@disabled={{@data.system}}
82+
/>
83+
</div>
84+
</@form.Container>
85+
86+
{{#if this.showJsonEditorModal}}
87+
<ModalJsonSchemaEditor
88+
@model={{hash
89+
value=this.responseFormatAsJSON
90+
updateValue=(fn this.updateResponseFormat @form)
91+
settingName=this.editorTitle
92+
jsonSchema=this.jsonSchema
93+
}}
94+
@closeModal={{this.closeModal}}
95+
/>
96+
{{/if}}
97+
</template>
98+
}

assets/stylesheets/modules/ai-bot/common/ai-persona.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@
5252
margin-bottom: 10px;
5353
font-size: var(--font-down-1);
5454
}
55+
56+
&__response-format {
57+
width: 100%;
58+
display: block;
59+
}
60+
61+
&__response-format-pre {
62+
margin-bottom: 0;
63+
white-space: pre-line;
64+
}
65+
66+
&__response-format-none {
67+
margin-bottom: 1em;
68+
margin-top: 0.5em;
69+
}
5570
}
5671

5772
.rag-options {

config/locales/client.en.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,13 @@ en:
321321
rag_conversation_chunks: "Search conversation chunks"
322322
rag_conversation_chunks_help: "The number of chunks to use for the RAG model searches. Increase to increase the amount of context the AI can use."
323323
persona_description: "Personas are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more personalized and engaging user experience."
324+
response_format:
325+
title: "JSON response format"
326+
no_format: "No JSON format specified"
327+
open_modal: "Edit"
328+
modal:
329+
root_title: "Response structure"
330+
key_title: "Key"
324331

325332
list:
326333
enabled: "AI Bot?"

db/fixtures/personas/603_ai_personas.rb

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

7373
persona.tools = tools.map { |name, value| [name, value] }
7474

75+
persona.response_format = instance.response_format
76+
7577
persona.system_prompt = instance.system_prompt
7678
persona.top_p = instance.top_p
7779
persona.temperature = instance.temperature
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
class AddResponseFormatJsonToPersonass < ActiveRecord::Migration[7.2]
3+
def change
4+
add_column :ai_personas, :response_format, :json, null: false, default: []
5+
end
6+
end

0 commit comments

Comments
 (0)