mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-07-09 07:33:30 +00:00
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.
262 lines
7.8 KiB
Ruby
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
|