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

Commit c0a2d4c

Browse files
authored
DEV: Use structured responses for summaries (#1252)
* DEV: Use structured responses for summaries * Fix system specs * Make response_format a first class citizen and update endpoints to support it * Response format can be specified in the persona * lint * switch to jsonb and make column nullable * Reify structured output chunks. Move JSON parsing to the depths of Completion * Switch to JsonStreamingTracker for partial JSON parsing
1 parent c6a307b commit c0a2d4c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+822
-68
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
@@ -325,19 +325,24 @@ def chat_preconditions
325325
end
326326

327327
def system_persona_unchangeable
328+
error_msg = I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona")
329+
328330
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
329331
description_changed?
330-
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
332+
errors.add(:base, error_msg)
331333
elsif tools_changed?
332334
old_tools = tools_change[0]
333335
new_tools = tools_change[1]
334336

335337
old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
336338
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
337339

338-
if old_tool_names != new_tool_names
339-
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
340-
end
340+
errors.add(:base, error_msg) if old_tool_names != new_tool_names
341+
elsif response_format_changed?
342+
old_format = response_format_change[0].map { |f| f["key"] }.to_set
343+
new_format = response_format_change[1].map { |f| f["key"] }.to_set
344+
345+
errors.add(:base, error_msg) if old_format != new_format
341346
end
342347
end
343348

@@ -395,6 +400,7 @@ def allowed_seeded_model
395400
# rag_llm_model_id :bigint
396401
# default_llm_id :bigint
397402
# question_consolidator_llm_id :bigint
403+
# response_format :jsonb
398404
#
399405
# Indexes
400406
#

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

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
@@ -323,6 +323,13 @@ en:
323323
rag_conversation_chunks: "Search conversation chunks"
324324
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."
325325
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."
326+
response_format:
327+
title: "JSON response format"
328+
no_format: "No JSON format specified"
329+
open_modal: "Edit"
330+
modal:
331+
root_title: "Response structure"
332+
key_title: "Key"
326333

327334
list:
328335
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, :jsonb
5+
end
6+
end

0 commit comments

Comments
 (0)