Skip to content

Commit 87f39d7

Browse files
committed
Response format can be specified in the persona
1 parent 66b7b66 commit 87f39d7

File tree

18 files changed

+255
-41
lines changed

18 files changed

+255
-41
lines changed

Diff for: app/controllers/discourse_ai/admin/ai_personas_controller.rb

+16
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

Diff for: app/models/ai_persona.rb

+10-4
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
#

Diff for: app/serializers/localized_ai_persona_serializer.rb

+2-1
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

Diff for: assets/javascripts/discourse/admin/models/ai-persona.js

+3
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
}

Diff for: assets/javascripts/discourse/components/ai-persona-editor.gjs

+3
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"}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 modalTitle() {
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
67+
@title={{i18n "discourse_ai.ai_persona.response_format.title"}}
68+
@format="large"
69+
>
70+
<div class="ai-persona-editor__response-format">
71+
{{#if (gt @data.response_format.length 0)}}
72+
<pre class="ai-persona-editor__response-format-pre">
73+
<code>{{this.displayJSON}}</code>
74+
</pre>
75+
{{else}}
76+
<div class="ai-persona-editor__response-format-none">{{i18n
77+
"discourse_ai.ai_persona.response_format.no_format"
78+
}}</div>
79+
{{/if}}
80+
81+
<@form.Button
82+
@action={{this.openModal}}
83+
@label="discourse_ai.ai_persona.response_format.open_modal"
84+
@disabled={{@data.system}}
85+
/>
86+
</div>
87+
</@form.Container>
88+
89+
{{#if this.showJsonEditorModal}}
90+
<ModalJsonSchemaEditor
91+
@model={{hash
92+
value=this.responseFormatAsJSON
93+
updateValue=(fn this.updateResponseFormat @form)
94+
settingName=this.modalTitle
95+
jsonSchema=this.jsonSchema
96+
}}
97+
@closeModal={{this.closeModal}}
98+
/>
99+
{{/if}}
100+
</template>
101+
}

Diff for: assets/stylesheets/modules/ai-bot/common/ai-persona.scss

+15
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 {

Diff for: config/locales/client.en.yml

+7
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,13 @@ en:
306306
rag_conversation_chunks: "Search conversation chunks"
307307
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."
308308
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."
309+
response_format:
310+
title: "JSON response format"
311+
no_format: "No JSON format specified"
312+
open_modal: "Edit"
313+
modal:
314+
root_title: "Response structure"
315+
key_title: "Key"
309316

310317
list:
311318
enabled: "AI Bot?"

Diff for: db/fixtures/personas/603_ai_personas.rb

+2
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
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

Diff for: lib/personas/bot.rb

+29
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def reply(context, llm_args: {}, &update_blk)
6868
llm_kwargs[:user] = user
6969
llm_kwargs[:temperature] = persona.temperature if persona.temperature
7070
llm_kwargs[:top_p] = persona.top_p if persona.top_p
71+
llm_kwargs[:response_format] = build_json_schema(
72+
persona.response_format,
73+
) if persona.response_format.present?
7174

7275
needs_newlines = false
7376
tools_ran = 0
@@ -176,6 +179,10 @@ def reply(context, llm_args: {}, &update_blk)
176179
embed_thinking(raw_context)
177180
end
178181

182+
def returns_json?
183+
persona.response_format.present?
184+
end
185+
179186
private
180187

181188
def embed_thinking(raw_context)
@@ -301,6 +308,28 @@ def build_placeholder(summary, details, custom_raw: nil)
301308

302309
placeholder
303310
end
311+
312+
def build_json_schema(response_format)
313+
properties =
314+
response_format.reduce({}) do |memo, format|
315+
memo[format[:key].to_sym] = { type: format[:type] }
316+
memo
317+
end
318+
319+
{
320+
type: "json_schema",
321+
json_schema: {
322+
name: "reply",
323+
schema: {
324+
type: "object",
325+
properties: properties,
326+
required: properties.keys.map(&:to_s),
327+
additionalProperties: false,
328+
},
329+
strict: true,
330+
},
331+
}
332+
end
304333
end
305334
end
306335
end

Diff for: lib/personas/bot_context.rb

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def initialize(
4949
@site_title = site_title
5050
@site_description = site_description
5151
@time = time
52+
@resource_url = resource_url
53+
5254
@feature_name = feature_name
5355

5456
if post

Diff for: lib/personas/persona.rb

+4
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def options
153153
{}
154154
end
155155

156+
def response_format
157+
[]
158+
end
159+
156160
def available_tools
157161
self
158162
.class

Diff for: lib/personas/short_summarizer.rb

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def system_prompt
2727
Where "xx" is replaced by the summary.
2828
PROMPT
2929
end
30+
31+
def response_format
32+
[{ key: "summary", type: "string" }]
33+
end
3034
end
3135
end
3236
end

Diff for: lib/personas/summarizer.rb

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def system_prompt
2828
Where "xx" is replaced by the summary.
2929
PROMPT
3030
end
31+
32+
def response_format
33+
[{ key: "summary", type: "string" }]
34+
end
3135
end
3236
end
3337
end

0 commit comments

Comments
 (0)