Skip to content

Commit

Permalink
work in progress artifact system revamp
Browse files Browse the repository at this point in the history
  • Loading branch information
SamSaffron committed Jan 28, 2025
1 parent c44c700 commit b976d5a
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 177 deletions.
49 changes: 49 additions & 0 deletions lib/ai_bot/artifact_update_strategies/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module ArtifactUpdateStrategies
class InvalidFormatError < StandardError
end
class Base
attr_reader :post, :user, :artifact, :artifact_version, :instructions, :llm

def initialize(llm:, post:, user:, artifact:, artifact_version:, instructions:)
@llm = llm
@post = post
@user = user
@artifact = artifact
@artifact_version = artifact_version
@instructions = instructions
end

def apply
changes = generate_changes
parsed_changes = parse_changes(changes)
apply_changes(parsed_changes)
end

private

def generate_changes
response = +""
llm.generate(build_prompt, user: user) { |partial| response << partial }
end

def build_prompt
# To be implemented by subclasses
raise NotImplementedError
end

def parse_changes(response)
# To be implemented by subclasses
raise NotImplementedError
end

def apply_changes(changes)
# To be implemented by subclasses
raise NotImplementedError
end
end
end
end
end
158 changes: 158 additions & 0 deletions lib/ai_bot/artifact_update_strategies/diff.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module ArtifactUpdateStrategies
class Diff < Base
private

def build_prompt
DiscourseAi::Completions::Prompt.new(
system_prompt,
messages: [
{ type: :user, content: current_artifact_content },
{ type: :model, content: "Please explain the changes you would like to generate:" },
{ type: :user, content: instructions },
],
post_id: post.id,
topic_id: post.topic_id,
)
end

def parse_changes(response)
sections = { html: nil, css: nil, javascript: nil }
current_section = nil
lines = []

response.each_line do |line|
case line
when /^--- (HTML|CSS|JavaScript) ---$/
sections[current_section] = lines.join if current_section && !lines.empty?
current_section = line.match(/^--- (.+) ---$/)[1].downcase.to_sym
lines = []
else
lines << line if current_section
end
end

sections[current_section] = lines.join if current_section && !lines.empty?

sections.transform_values do |content|
next nil if content.nil?
blocks = extract_search_replace_blocks(content)
raise InvalidFormatError, "Invalid format in #{current_section} section" if blocks.nil?
blocks
end
end

def apply_changes(changes)
source = artifact_version || artifact
updated_content = { js: source.js, html: source.html, css: source.css }

%i[html css javascript].each do |section|
blocks = changes[section]
next unless blocks

content = source.public_send(section == :javascript ? :js : section)
blocks.each do |block|
content =
DiscourseAi::Utils::DiffUtils::SimpleDiff.apply(
content,
block[:search],
block[:replace],
)
end
updated_content[section == :javascript ? :js : section] = content
end

artifact.create_new_version(
html: updated_content[:html],
css: updated_content[:css],
js: updated_content[:js],
change_description: instructions,
)
end

private

def extract_search_replace_blocks(content)
return nil if content.blank?

blocks = []
remaining = content

while remaining =~ /<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE/m
blocks << { search: $1, replace: $2 }
remaining = $'
end

blocks.empty? ? nil : blocks
end

def system_prompt
<<~PROMPT
You are a web development expert generating precise search/replace changes for updating HTML, CSS, and JavaScript code.
Important rules:
1. Use the format <<<<<<< SEARCH / ======= / >>>>>>> REPLACE for each change
2. You can specify multiple search/replace blocks per section
3. Generate three sections: HTML, CSS, and JavaScript
4. Only include sections that have changes
5. Keep changes minimal and focused
6. Use exact matches for the search content
Format:
--- HTML ---
(changes or empty if no changes)
--- CSS ---
(changes or empty if no changes)
--- JavaScript ---
(changes or empty if no changes)
Example - Multiple changes in one file:
--- JavaScript ---
<<<<<<< SEARCH
console.log('old1');
=======
console.log('new1');
>>>>>>> REPLACE
<<<<<<< SEARCH
console.log('old2');
=======
console.log('new2');
>>>>>>> REPLACE
Example - CSS with multiple blocks:
--- CSS ---
<<<<<<< SEARCH
.button { color: blue; }
=======
.button { color: red; }
>>>>>>> REPLACE
<<<<<<< SEARCH
.text { font-size: 12px; }
=======
.text { font-size: 16px; }
>>>>>>> REPLACE
PROMPT
end

def current_artifact_content
source = artifact_version || artifact
<<~CONTENT
Current artifact code:
--- HTML ---
#{source.html}
--- CSS ---
#{source.css}
--- JavaScript ---
#{source.js}
CONTENT
end
end
end
end
end
144 changes: 144 additions & 0 deletions lib/ai_bot/artifact_update_strategies/full.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module ArtifactUpdateStrategies
class Full < Base
private

def build_prompt
DiscourseAi::Completions::Prompt.new(
system_prompt,
messages: [
{ type: :user, content: "#{current_artifact_content}\n\n\n#{instructions}" },
],
post_id: post.id,
topic_id: post.topic_id,
)
end

def parse_changes(response)
sections = { html: nil, css: nil, javascript: nil }
current_section = nil
lines = []

response.each_line do |line|
case line
when /^\[(HTML|CSS|JavaScript)\]$/
sections[current_section] = lines.join if current_section && !lines.empty?
current_section = line.match(/^\[(.+)\]$/)[1].downcase.to_sym
lines = []
when %r{^\[/(HTML|CSS|JavaScript)\]$}
sections[current_section] = lines.join if current_section && !lines.empty?
current_section = nil
lines = []
else
lines << line if current_section
end
end

sections
end

def apply_changes(changes)
source = artifact_version || artifact
updated_content = { js: source.js, html: source.html, css: source.css }

%i[html css javascript].each do |section|
content = changes[section]&.strip
next if content.blank?
updated_content[section == :javascript ? :js : section] = content
end

artifact.create_new_version(
html: updated_content[:html],
css: updated_content[:css],
js: updated_content[:js],
change_description: instructions,
)
end

private

def system_prompt
<<~PROMPT
You are a web development expert generating updated HTML, CSS, and JavaScript code.
Important rules:
1. Provide full source code for each changed section
2. Generate up to three sections: HTML, CSS, and JavaScript
3. Only include sections that need changes
4. Keep changes focused on the requirements
5. NEVER EVER BE LAZY, always include ALL the source code with any update you make. If you are lazy you will break the artifact.
6. Do not print out any reasoning, just the changed code, you will be parsed via a program.
7. Sections must start and end with exact tags: [HTML] [/HTML], [CSS] [/CSS], [JavaScript] [/JavaScript]
Always adhere to the format when replying:
[HTML]
complete html code, omit if no changes
[/HTML]
[CSS]
complete css code, omit if no changes
[/CSS]
[JavaScript]
complete js code, omit if no changes
[/JavaScript]
Examples:
Example 1 (HTML only change):
[HTML]
<div class="container">
<h1>Title</h1>
</div>
[/HTML]
Example 2 (CSS and JavaScript changes):
[CSS]
.container { padding: 20px; }
.title { color: blue; }
[/CSS]
[JavaScript]
function init() {
console.log("loaded");
}
[/JavaScript]
Example 3 (All sections):
[HTML]
<div id="app"></div>
[/HTML]
[CSS]
#app { margin: 0; }
[/CSS]
[JavaScript]
const app = document.getElementById("app");
[/JavaScript]
PROMPT
end

def current_artifact_content
source = artifact_version || artifact
<<~CONTENT
Current artifact code:
[HTML]
#{source.html}
[/HTML]
[CSS]
#{source.css}
[/CSS]
[JavaScript]
#{source.js}
[/JavaScript]
CONTENT
end
end
end
end
end
4 changes: 4 additions & 0 deletions lib/ai_bot/tools/create_artifact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def invoke
end
end

def chain_next_response?
false
end

def description_args
{ name: parameters[:name], specification: parameters[:specification] }
end
Expand Down
Loading

0 comments on commit b976d5a

Please sign in to comment.