mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-11-09 18:58:59 +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
|