discourse-ai/lib/personas/tools/update_artifact.rb

262 lines
7.8 KiB
Ruby

# frozen_string_literal: true
module DiscourseAi
module Personas
module Tools
class UpdateArtifact < Tool
def self.name
"update_artifact"
end
def self.signature
{
name: "update_artifact",
description: "Updates an existing web artifact",
parameters: [
{
name: "artifact_id",
description: "The ID of the artifact to update",
type: "integer",
required: true,
},
{
name: "instructions",
description: "Clear instructions on what changes need to be made to the artifact.",
type: "string",
required: true,
},
{
name: "version",
description:
"The version number of the artifact to update, if not supplied latest version will be updated",
type: "integer",
required: false,
},
],
}
end
def self.inject_prompt(prompt:, context:, persona:)
return if persona.options["do_not_echo_artifact"].to_s == "true"
# we inject the current artifact content into the last user message
if topic_id = context.topic_id
posts = Post.where(topic_id: topic_id)
artifact = AiArtifact.order("id desc").where(post: posts).first
if artifact
latest_version = artifact.versions.order(version_number: :desc).first
current = latest_version || artifact
artifact_source = <<~MSG
Current Artifact:
### HTML
```html
#{current.html}
```
### CSS
```css
#{current.css}
```
### JavaScript
```javascript
#{current.js}
```
MSG
last_message = prompt.messages.last
last_message[:content] = "#{artifact_source}\n\n#{last_message[:content]}"
end
end
end
def self.accepted_options
[
option(:editor_llm, type: :llm),
option(:update_algorithm, type: :enum, values: %w[diff full], default: "diff"),
option(:do_not_echo_artifact, type: :boolean, default: true),
]
end
def self.allow_partial_tool_calls?
true
end
def partial_invoke
in_progress(instructions: parameters[:instructions]) if parameters[:instructions].present?
end
def in_progress(instructions:, source: nil)
source = (<<~HTML) if source.present?
### Source
````
#{source}
````
HTML
self.custom_raw = <<~HTML
<details>
<summary>Thinking...</summary>
### Instructions
````
#{instructions}
````
#{source}
</details>
HTML
end
def invoke
post = Post.find_by(id: context.post_id)
return error_response("No post context found") unless post
artifact = AiArtifact.find_by(id: parameters[:artifact_id])
return error_response("Artifact not found") unless artifact
artifact_version = nil
if version = parameters[:version]
artifact_version = artifact.versions.find_by(version_number: version)
# we could tell llm it is confused here if artifact version is not there
# but let's just fix it transparently which saves an llm call
end
artifact_version ||= artifact.versions.order(version_number: :desc).first
if artifact.post.topic.id != post.topic.id
return error_response("Attempting to update an artifact you are not allowed to")
end
llm =
(
options[:editor_llm].present? &&
LlmModel.find_by(id: options[:editor_llm].to_i)&.to_llm
) || self.llm
strategy =
(
if options[:update_algorithm] == "diff"
ArtifactUpdateStrategies::Diff
else
ArtifactUpdateStrategies::Full
end
)
begin
instructions = parameters[:instructions]
partial_response = +""
new_version =
strategy
.new(
llm: llm,
post: post,
user: post.user,
artifact: artifact,
artifact_version: artifact_version,
instructions: instructions,
cancel_manager: context.cancel_manager,
)
.apply do |progress|
partial_response << progress
in_progress(instructions: instructions, source: partial_response)
# force in progress to render
yield nil, true
end
update_custom_html(
artifact: artifact,
artifact_version: artifact_version,
new_version: new_version,
)
success_response(artifact, new_version)
rescue StandardError => e
error_response(e.message)
end
end
def chain_next_response?
false
end
private
def line_based_markdown_diff(before, after)
# Split into lines
before_lines = before.split("\n")
after_lines = after.split("\n")
# Use ONPDiff for line-level comparison
diff = ONPDiff.new(before_lines, after_lines).diff
# Build markdown output
result = ["```diff"]
diff.each do |line, status|
case status
when :common
result << " #{line}"
when :delete
result << "-#{line}"
when :add
result << "+#{line}"
end
end
result << "```"
result.join("\n")
end
def update_custom_html(artifact:, artifact_version:, new_version:)
content = []
if new_version.change_description.present?
content << [
:description,
"[details='#{I18n.t("discourse_ai.ai_artifact.change_description")}']\n\n````\n#{new_version.change_description}\n````\n\n[/details]",
]
end
content << [nil, "[details='#{I18n.t("discourse_ai.ai_artifact.view_changes")}']"]
%w[html css js].each do |type|
source = artifact_version || artifact
old_content = source.public_send(type)
new_content = new_version.public_send(type)
if old_content != new_content
diff = line_based_markdown_diff(old_content, new_content)
content << [nil, "### #{type.upcase} Changes\n#{diff}"]
end
end
content << [nil, "[/details]"]
content << [
:preview,
"### Preview\n\n<div class=\"ai-artifact\" data-ai-artifact-version=\"#{new_version.version_number}\" data-ai-artifact-id=\"#{artifact.id}\"></div>",
]
self.custom_raw = content.map { |c| c[1] }.join("\n\n")
end
def success_response(artifact, version)
{
status: "success",
artifact_id: artifact.id,
version: version.version_number,
message: "Artifact updated successfully and rendered to user.",
}
end
def error_response(message)
self.custom_raw = ""
{ status: "error", error: message }
end
end
end
end
end