discourse-ai/lib/personas/tools/update_artifact.rb
Sam c34fcc8a95
FEATURE: forum researcher persona for deep research (#1313)
This commit introduces a new Forum Researcher persona specialized in deep forum content analysis along with comprehensive improvements to our AI infrastructure.

Key additions:

    New Forum Researcher persona with advanced filtering and analysis capabilities
    Robust filtering system supporting tags, categories, dates, users, and keywords
    LLM formatter to efficiently process and chunk research results

Infrastructure improvements:

    Implemented CancelManager class to centrally manage AI completion cancellations
    Replaced callback-based cancellation with a more robust pattern
    Added systematic cancellation monitoring with callbacks

Other improvements:

    Added configurable default_enabled flag to control which personas are enabled by default
    Updated translation strings for the new researcher functionality
    Added comprehensive specs for the new components

    Renames Researcher -> Web Researcher

This change makes our AI platform more stable while adding powerful research capabilities that can analyze forum trends and surface relevant content.
2025-05-14 12:36:16 +10:00

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