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

Commit a7d032f

Browse files
authored
DEV: artifact system update (#1096)
### Why This pull request fundamentally restructures how AI bots create and update web artifacts to address critical limitations in the previous approach: 1. **Improved Artifact Context for LLMs**: Previously, artifact creation and update tools included the *entire* artifact source code directly in the tool arguments. This overloaded the Language Model (LLM) with raw code, making it difficult for the LLM to maintain a clear understanding of the artifact's current state when applying changes. The LLM would struggle to differentiate between the base artifact and the requested modifications, leading to confusion and less effective updates. 2. **Reduced Token Usage and History Bloat**: Including the full artifact source code in every tool interaction was extremely token-inefficient. As conversations progressed, this redundant code in the history consumed a significant number of tokens unnecessarily. This not only increased costs but also diluted the context for the LLM with less relevant historical information. 3. **Enabling Updates for Large Artifacts**: The lack of a practical diff or targeted update mechanism made it nearly impossible to efficiently update larger web artifacts. Sending the entire source code for every minor change was both computationally expensive and prone to errors, effectively blocking the use of AI bots for meaningful modifications of complex artifacts. **This pull request addresses these core issues by**: * Introducing methods for the AI bot to explicitly *read* and understand the current state of an artifact. * Implementing efficient update strategies that send *targeted* changes rather than the entire artifact source code. * Providing options to control the level of artifact context included in LLM prompts, optimizing token usage. ### What The main changes implemented in this PR to resolve the above issues are: 1. **`Read Artifact` Tool for Contextual Awareness**: - A new `read_artifact` tool is introduced, enabling AI bots to fetch and process the current content of a web artifact from a given URL (local or external). - This provides the LLM with a clear and up-to-date representation of the artifact's HTML, CSS, and JavaScript, improving its understanding of the base to be modified. - By cloning local artifacts, it allows the bot to work with a fresh copy, further enhancing context and control. 2. **Refactored `Update Artifact` Tool with Efficient Strategies**: - The `update_artifact` tool is redesigned to employ more efficient update strategies, minimizing token usage and improving update precision: - **`diff` strategy**: Utilizes a search-and-replace diff algorithm to apply only the necessary, targeted changes to the artifact's code. This significantly reduces the amount of code sent to the LLM and focuses its attention on the specific modifications. - **`full` strategy**: Provides the option to replace the entire content sections (HTML, CSS, JavaScript) when a complete rewrite is required. - Tool options enhance the control over the update process: - `editor_llm`: Allows selection of a specific LLM for artifact updates, potentially optimizing for code editing tasks. - `update_algorithm`: Enables choosing between `diff` and `full` update strategies based on the nature of the required changes. - `do_not_echo_artifact`: Defaults to true, and by *not* echoing the artifact in prompts, it further reduces token consumption in scenarios where the LLM might not need the full artifact context for every update step (though effectiveness might be slightly reduced in certain update scenarios). 3. **System and General Persona Tool Option Visibility and Customization**: - Tool options, including those for system personas, are made visible and editable in the admin UI. This allows administrators to fine-tune the behavior of all personas and their tools, including setting specific LLMs or update algorithms. This was previously limited or hidden for system personas. 4. **Centralized and Improved Content Security Policy (CSP) Management**: - The CSP for AI artifacts is consolidated and made more maintainable through the `ALLOWED_CDN_SOURCES` constant. This improves code organization and future updates to the allowed CDN list, while maintaining the existing security posture. 5. **Codebase Improvements**: - Refactoring of diff utilities, introduction of strategy classes, enhanced error handling, new locales, and comprehensive testing all contribute to a more robust, efficient, and maintainable artifact management system. By addressing the issues of LLM context confusion, token inefficiency, and the limitations of updating large artifacts, this pull request significantly improves the practicality and effectiveness of AI bots in managing web artifacts within Discourse.
1 parent 7ad9223 commit a7d032f

File tree

35 files changed

+2265
-548
lines changed

35 files changed

+2265
-548
lines changed

app/controllers/discourse_ai/ai_bot/artifacts_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def show
1212
artifact = AiArtifact.find(params[:id])
1313

1414
post = Post.find_by(id: artifact.post_id)
15-
if artifact.metadata&.dig("public")
15+
if artifact.public?
1616
# no guardian needed
1717
else
1818
raise Discourse::NotFound if !post&.topic&.private_message?
@@ -81,7 +81,7 @@ def show
8181
response.headers.delete("X-Frame-Options")
8282
response.headers[
8383
"Content-Security-Policy"
84-
] = "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com https://ajax.googleapis.com https://cdn.jsdelivr.net;"
84+
] = "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' #{AiArtifact::ALLOWED_CDN_SOURCES.join(" ")};"
8585
response.headers["X-Robots-Tag"] = "noindex"
8686

8787
# Render the content

app/models/ai_artifact.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ class AiArtifact < ActiveRecord::Base
88
validates :css, length: { maximum: 65_535 }
99
validates :js, length: { maximum: 65_535 }
1010

11+
ALLOWED_CDN_SOURCES = %w[
12+
https://cdn.jsdelivr.net
13+
https://cdnjs.cloudflare.com
14+
https://unpkg.com
15+
https://ajax.googleapis.com
16+
https://d3js.org
17+
https://code.jquery.com
18+
]
19+
1120
def self.iframe_for(id, version = nil)
1221
<<~HTML
1322
<div class='ai-artifact'>
@@ -70,6 +79,10 @@ def create_new_version(html: nil, css: nil, js: nil, change_description: nil)
7079

7180
version
7281
end
82+
83+
def public?
84+
!!metadata&.dig("public")
85+
end
7386
end
7487

7588
# == Schema Information

app/models/ai_persona.rb

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ def class_instance
130130
tool_details
131131
]
132132

133-
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
134-
135133
instance_attributes = {}
136134
attributes.each do |attr|
137135
value = self.read_attribute(attr)
@@ -140,14 +138,6 @@ def class_instance
140138

141139
instance_attributes[:username] = user&.username_lower
142140

143-
if persona_class
144-
instance_attributes.each do |key, value|
145-
# description/name are localized
146-
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
147-
end
148-
return persona_class
149-
end
150-
151141
options = {}
152142
force_tool_use = []
153143

@@ -180,6 +170,16 @@ def class_instance
180170
klass
181171
end
182172

173+
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
174+
if persona_class
175+
instance_attributes.each do |key, value|
176+
# description/name are localized
177+
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
178+
end
179+
persona_class.define_method(:options) { options }
180+
return persona_class
181+
end
182+
183183
ai_persona_id = self.id
184184

185185
Class.new(DiscourseAi::AiBot::Personas::Persona) do
@@ -264,9 +264,19 @@ def chat_preconditions
264264
end
265265

266266
def system_persona_unchangeable
267-
if top_p_changed? || temperature_changed? || system_prompt_changed? || tools_changed? ||
268-
name_changed? || description_changed?
267+
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
268+
description_changed?
269269
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
270+
elsif tools_changed?
271+
old_tools = tools_change[0]
272+
new_tools = tools_change[1]
273+
274+
old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
275+
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
276+
277+
if old_tool_names != new_tool_names
278+
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
279+
end
270280
end
271281
end
272282

app/serializers/ai_tool_serializer.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ def help
2222
def options
2323
options = {}
2424
object.accepted_options.each do |option|
25-
options[option.name] = {
25+
processed_option = {
2626
name: option.localized_name,
2727
description: option.localized_description,
2828
type: option.type,
2929
}
30+
processed_option[:values] = option.values if option.values.present?
31+
processed_option[:default] = option.default if option.default.present?
32+
options[option.name] = processed_option
3033
end
3134
options
3235
end

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const SYSTEM_ATTRIBUTES = [
4141
"enabled",
4242
"system",
4343
"priority",
44+
"tools",
4445
"user_id",
4546
"default_llm",
4647
"force_default_llm",

assets/javascripts/discourse/components/ai-llm-selector.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ export default class AiLlmSelector extends ComboBox {
1515

1616
@computed
1717
get content() {
18+
const blankName =
19+
this.attrs.blankName || i18n("discourse_ai.ai_persona.no_llm_selected");
1820
return [
1921
{
2022
id: "blank",
21-
name: i18n("discourse_ai.ai_persona.no_llm_selected"),
23+
name: blankName,
2224
},
2325
].concat(this.llms);
2426
}

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -420,13 +420,12 @@ export default class PersonaEditor extends Component {
420420
</div>
421421
{{/if}}
422422
{{/if}}
423-
{{#unless this.editingModel.system}}
424-
<AiPersonaToolOptions
425-
@persona={{this.editingModel}}
426-
@tools={{this.selectedToolNames}}
427-
@allTools={{@personas.resultSetMeta.tools}}
428-
/>
429-
{{/unless}}
423+
<AiPersonaToolOptions
424+
@persona={{this.editingModel}}
425+
@tools={{this.selectedToolNames}}
426+
@llms={{@personas.resultSetMeta.llms}}
427+
@allTools={{@personas.resultSetMeta.tools}}
428+
/>
430429
<div class="control-group">
431430
<label>{{i18n "discourse_ai.ai_persona.allowed_groups"}}</label>
432431
<GroupChooser

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,75 @@ import Component from "@glimmer/component";
22
import { Input } from "@ember/component";
33
import { on } from "@ember/modifier";
44
import { action } from "@ember/object";
5+
import { eq } from "truth-helpers";
6+
import { i18n } from "discourse-i18n";
7+
import AiLlmSelector from "./ai-llm-selector";
58

69
export default class AiPersonaToolOptionEditor extends Component {
710
get isBoolean() {
811
return this.args.option.type === "boolean";
912
}
1013

14+
get isEnum() {
15+
return this.args.option.type === "enum";
16+
}
17+
18+
get isLlm() {
19+
return this.args.option.type === "llm";
20+
}
21+
1122
get selectedValue() {
1223
return this.args.option.value.value === "true";
1324
}
1425

26+
get selectedLlm() {
27+
if (this.args.option.value.value) {
28+
return `custom:${this.args.option.value.value}`;
29+
} else {
30+
return "blank";
31+
}
32+
}
33+
34+
set selectedLlm(value) {
35+
if (value === "blank") {
36+
this.args.option.value.value = null;
37+
} else {
38+
this.args.option.value.value = value.replace("custom:", "");
39+
}
40+
}
41+
1542
@action
1643
onCheckboxChange(event) {
1744
this.args.option.value.value = event.target.checked ? "true" : "false";
1845
}
1946

47+
@action
48+
onSelectOption(event) {
49+
this.args.option.value.value = event.target.value;
50+
}
51+
2052
<template>
2153
<div class="control-group ai-persona-tool-option-editor">
2254
<label>
2355
{{@option.name}}
2456
</label>
2557
<div class="">
26-
{{#if this.isBoolean}}
58+
{{#if this.isEnum}}
59+
<select name="input" {{on "change" this.onSelectOption}}>
60+
{{#each @option.values as |value|}}
61+
<option value={{value}} selected={{eq value @option.value.value}}>
62+
{{value}}
63+
</option>
64+
{{/each}}
65+
</select>
66+
{{else if this.isLlm}}
67+
<AiLlmSelector
68+
class="ai-persona-tool-option-editor__llms"
69+
@value={{this.selectedLlm}}
70+
@llms={{@llms}}
71+
@blankName={{i18n "discourse_ai.ai_persona.use_parent_llm"}}
72+
/>
73+
{{else if this.isBoolean}}
2774
<input
2875
type="checkbox"
2976
checked={{this.selectedValue}}

assets/javascripts/discourse/components/ai-persona-tool-options.gjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ export default class AiPersonaToolOptions extends Component {
6767
</div>
6868
<div class="ai-persona-editor__tool-option-options">
6969
{{#each toolOption.options as |option|}}
70-
<AiPersonaToolOptionEditor @option={{option}} />
70+
<AiPersonaToolOptionEditor
71+
@option={{option}}
72+
@llms={{@llms}}
73+
/>
7174
{{/each}}
7275
</div>
7376
</div>

config/locales/client.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ en:
213213
edit: "Edit"
214214
description: "Description"
215215
no_llm_selected: "No language model selected"
216+
use_parent_llm: "Use personas language model"
216217
max_context_posts: "Max context posts"
217218
max_context_posts_help: "The maximum number of posts to use as context for the AI when responding to a user. (empty for default)"
218219
vision_enabled: Vision enabled

0 commit comments

Comments
 (0)