# 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
Thinking... ### Instructions ```` #{instructions} ```` #{source}
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
", ] 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