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

Commit 72559c7

Browse files
committed
testing and prompt engineering
1 parent 3b752d0 commit 72559c7

File tree

4 files changed

+287
-16
lines changed

4 files changed

+287
-16
lines changed

app/models/ai_artifact_version.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ class AiArtifactVersion < ActiveRecord::Base
44
validates :html, length: { maximum: 65_535 }
55
validates :css, length: { maximum: 65_535 }
66
validates :js, length: { maximum: 65_535 }
7+
8+
# used when generating test cases
9+
def write_to(path)
10+
css_path = "#{path}/main.css"
11+
html_path = "#{path}/main.html"
12+
js_path = "#{path}/main.js"
13+
instructions_path = "#{path}/instructions.txt"
14+
15+
File.write(css_path, css)
16+
File.write(html_path, html)
17+
File.write(js_path, js)
18+
File.write(instructions_path, change_description)
19+
end
720
end
821

922
# == Schema Information

lib/ai_bot/artifact_update_strategies/diff.rb

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,16 @@ def apply_changes(changes)
5858
content = source.public_send(section == :javascript ? :js : section)
5959
blocks.each do |block|
6060
begin
61-
content =
62-
DiscourseAi::Utils::DiffUtils::SimpleDiff.apply(
63-
content,
64-
block[:search],
65-
block[:replace],
66-
)
61+
if !block[:search]
62+
content = block[:replace]
63+
else
64+
content =
65+
DiscourseAi::Utils::DiffUtils::SimpleDiff.apply(
66+
content,
67+
block[:search],
68+
block[:replace],
69+
)
70+
end
6771
rescue DiscourseAi::Utils::DiffUtils::SimpleDiff::NoMatchError
6872
@failed_searches << { section: section, search: block[:search] }
6973
# TODO, we may need to inform caller here, LLM made a mistake which it
@@ -85,7 +89,8 @@ def apply_changes(changes)
8589
private
8690

8791
def extract_search_replace_blocks(content)
88-
return nil if content.blank?
92+
return nil if content.blank? || content.to_s.strip.downcase.match?(/^\(?no changes?\)?$/m)
93+
return [{ replace: content }] if !content.match?(/<<+\s*SEARCH/)
8994

9095
blocks = []
9196
remaining = content
@@ -116,25 +121,26 @@ def system_prompt
116121
2. DO NOT modify the markers or add spaces around them
117122
3. DO NOT add explanations or comments within sections
118123
4. ONLY include [HTML], [CSS], and [JavaScript] sections if they have changes
119-
5. Keep changes minimal and focused
120-
6. HTML should not include <html>, <head>, or <body> tags, it is injected into a template
121-
7. When specifying a SEARCH block, ALWAYS keep it 8 lines or less, you will be interrupted and a retry will be required if you exceed this limit
122-
8. NEVER EVER ask followup questions, ALL changes must be performed in a single response, you are consumed via an API, there is no opportunity for humans in the loop
123-
9. When performing a non-contiguous search, ALWAYS use ... to denote the skipped lines
124-
10. Be mindful that ... non-contiguous search is not greedy, the following line will only match the first occurrence of the search block
124+
5. HTML should not include <html>, <head>, or <body> tags, it is injected into a template
125+
6. When specifying a SEARCH block, ALWAYS keep it 8 lines or less, you will be interrupted and a retry will be required if you exceed this limit
126+
7. NEVER EVER ask followup questions, ALL changes must be performed in a single response, you are consumed via an API, there is no opportunity for humans in the loop
127+
8. When performing a non-contiguous search, ALWAYS use ... to denote the skipped lines
128+
9. Be mindful that ... non-contiguous search is not greedy, the following line will only match the first occurrence of the search block
129+
10. Never mix a full section replacement with a search/replace block in the same section
130+
11. ALWAYS skip sections you to not want to change, do not include them in the response
125131
126132
JavaScript libraries must be sourced from the following CDNs, otherwise CSP will reject it:
127133
#{AiArtifact::ALLOWED_CDN_SOURCES.join("\n")}
128134
129135
Reply Format:
130136
[HTML]
131-
(changes or empty if no changes)
137+
(changes or empty if no changes or entire HTML)
132138
[/HTML]
133139
[CSS]
134-
(changes or empty if no changes)
140+
(changes or empty if no changes or entire CSS)
135141
[/CSS]
136142
[JavaScript]
137-
(changes or empty if no changes)
143+
(changes or empty if no changes or entire JavaScript)
138144
[/JavaScript]
139145
140146
Example - Multiple changes in one file:
@@ -209,6 +215,25 @@ def system_prompt
209215
}
210216
[/CSS]
211217
218+
Example - full HTML replacement:
219+
220+
[HTML]
221+
<div>something old</div>
222+
<div>another somethin old</div>
223+
[/HTML]
224+
225+
output:
226+
227+
[HTML]
228+
<div>something new</div>
229+
[/HTML]
230+
231+
result:
232+
[HTML]
233+
<div>something new</div>
234+
[/HTML]
235+
236+
212237
PROMPT
213238
end
214239

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
Fabricator(:ai_artifact) do
3+
user
4+
post
5+
name { sequence(:name) { |i| "artifact_#{i}" } }
6+
html { "<div>Test Content</div>" }
7+
css { ".test { color: blue; }" }
8+
js { "console.log('test');" }
9+
metadata { { public: false } }
10+
end
11+
12+
Fabricator(:ai_artifact_version) do
13+
ai_artifact
14+
version_number { sequence(:version_number) { |i| i } }
15+
html { "<div>Version Content</div>" }
16+
css { ".version { color: red; }" }
17+
js { "console.log('version');" }
18+
change_description { "Test change" }
19+
end
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe DiscourseAi::AiBot::ArtifactUpdateStrategies::Diff do
4+
fab!(:user)
5+
fab!(:post)
6+
fab!(:artifact) { Fabricate(:ai_artifact) }
7+
fab!(:llm_model)
8+
9+
let(:llm) { llm_model.to_llm }
10+
let(:instructions) { "Update the button color to red" }
11+
12+
let(:strategy) do
13+
described_class.new(
14+
llm: llm,
15+
post: post,
16+
user: user,
17+
artifact: artifact,
18+
artifact_version: nil,
19+
instructions: instructions,
20+
)
21+
end
22+
23+
describe "#apply" do
24+
it "processes simple search/replace blocks" do
25+
original_css = ".button { color: blue; }"
26+
artifact.update!(css: original_css)
27+
28+
response = <<~RESPONSE
29+
[CSS]
30+
<<<<<<< SEARCH
31+
.button { color: blue; }
32+
=======
33+
.button { color: red; }
34+
>>>>>>> REPLACE
35+
[/CSS]
36+
RESPONSE
37+
38+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
39+
40+
expect(artifact.versions.last.css).to eq(".button { color: red; }")
41+
end
42+
43+
it "handles multiple search/replace blocks in the same section" do
44+
original_css = <<~CSS
45+
.button { color: blue; }
46+
.text { font-size: 12px; }
47+
CSS
48+
49+
artifact.update!(css: original_css)
50+
51+
response = <<~RESPONSE
52+
[CSS]
53+
<<<<<<< SEARCH
54+
.button { color: blue; }
55+
=======
56+
.button { color: red; }
57+
>>>>>>> REPLACE
58+
<<<<<<< SEARCH
59+
.text { font-size: 12px; }
60+
=======
61+
.text { font-size: 16px; }
62+
>>>>>>> REPLACE
63+
[/CSS]
64+
RESPONSE
65+
66+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
67+
68+
expected = <<~CSS.strip
69+
.button { color: red; }
70+
.text { font-size: 16px; }
71+
CSS
72+
73+
expect(artifact.versions.last.css.strip).to eq(expected.strip)
74+
end
75+
76+
it "handles non-contiguous search/replace using ..." do
77+
original_css = <<~CSS
78+
body {
79+
color: red;
80+
}
81+
.button {
82+
color: blue;
83+
}
84+
.alert {
85+
background-color: green;
86+
}
87+
CSS
88+
89+
artifact.update!(css: original_css)
90+
91+
response = <<~RESPONSE
92+
[CSS]
93+
<<<<<<< SEARCH
94+
body {
95+
...
96+
background-color: green;
97+
}
98+
=======
99+
body {
100+
color: red;
101+
}
102+
>>>>>>> REPLACE
103+
[/CSS]
104+
RESPONSE
105+
106+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
107+
108+
expect(artifact.versions.last.css).to eq("body {\n color: red;\n}")
109+
end
110+
111+
it "tracks failed searches" do
112+
original_css = ".button { color: blue; }"
113+
artifact.update!(css: original_css)
114+
115+
response = <<~RESPONSE
116+
[CSS]
117+
<<<<<<< SEARCH
118+
.button { color: green; }
119+
=======
120+
.button { color: red; }
121+
>>>>>>> REPLACE
122+
[/CSS]
123+
RESPONSE
124+
125+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
126+
127+
expect(strategy.failed_searches).to contain_exactly(
128+
{ section: :css, search: ".button { color: green; }" },
129+
)
130+
expect(artifact.versions.last.css).to eq(original_css)
131+
end
132+
133+
it "handles complete section replacements" do
134+
original_html = "<div>old content</div>"
135+
artifact.update!(html: original_html)
136+
137+
response = <<~RESPONSE
138+
[HTML]
139+
<div>new content</div>
140+
[/HTML]
141+
RESPONSE
142+
143+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
144+
145+
expect(artifact.versions.last.html.strip).to eq("<div>new content</div>")
146+
end
147+
148+
it "ignores empty or 'no changes' sections part 1" do
149+
original = {
150+
html: "<div>content</div>",
151+
css: ".button { color: blue; }",
152+
js: "console.log('test');",
153+
}
154+
155+
artifact.update!(html: original[:html], css: original[:css], js: original[:js])
156+
157+
response = <<~RESPONSE
158+
[HTML]
159+
no changes
160+
[/HTML]
161+
[CSS]
162+
(NO CHANGES)
163+
[/CSS]
164+
[JavaScript]
165+
<<<<<<< SEARCH
166+
console.log('test');
167+
=======
168+
console.log('(no changes)');
169+
>>>>>>> REPLACE
170+
[/JavaScript]
171+
RESPONSE
172+
173+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
174+
175+
version = artifact.versions.last
176+
expect(version.html).to eq(original[:html])
177+
expect(version.css).to eq(original[:css])
178+
expect(version.js).to eq("console.log('(no changes)');")
179+
end
180+
181+
it "ignores empty or 'no changes' section part 2" do
182+
original = {
183+
html: "<div>content</div>",
184+
css: ".button { color: blue; }",
185+
js: "console.log('test');",
186+
}
187+
188+
artifact.update!(html: original[:html], css: original[:css], js: original[:js])
189+
190+
response = <<~RESPONSE
191+
[HTML]
192+
(no changes)
193+
[/HTML]
194+
[CSS]
195+
196+
[/CSS]
197+
[JavaScript]
198+
<<<<<<< SEARCH
199+
console.log('test');
200+
=======
201+
console.log('updated');
202+
>>>>>>> REPLACE
203+
[/JavaScript]
204+
RESPONSE
205+
206+
DiscourseAi::Completions::Llm.with_prepared_responses([response]) { strategy.apply }
207+
208+
version = artifact.versions.last
209+
expect(version.html).to eq(original[:html])
210+
expect(version.css).to eq(original[:css])
211+
expect(version.js).to eq("console.log('updated');")
212+
end
213+
end
214+
end

0 commit comments

Comments
 (0)