discourse-ai/lib/completions/xml_tool_processor.rb
Sam 0d7f353284
FEATURE: AI artifacts (#898)
This is a significant PR that introduces AI Artifacts functionality to the discourse-ai plugin along with several other improvements. Here are the key changes:

1. AI Artifacts System:
   - Adds a new `AiArtifact` model and database migration
   - Allows creation of web artifacts with HTML, CSS, and JavaScript content
   - Introduces security settings (`strict`, `lax`, `disabled`) for controlling artifact execution
   - Implements artifact rendering in iframes with sandbox protection
   - New `CreateArtifact` tool for AI to generate interactive content

2. Tool System Improvements:
   - Adds support for partial tool calls, allowing incremental updates during generation
   - Better handling of tool call states and progress tracking
   - Improved XML tool processing with CDATA support
   - Fixes for tool parameter handling and duplicate invocations

3. LLM Provider Updates:
   - Updates for Anthropic Claude models with correct token limits
   - Adds support for native/XML tool modes in Gemini integration
   - Adds new model configurations including Llama 3.1 models
   - Improvements to streaming response handling

4. UI Enhancements:
   - New artifact viewer component with expand/collapse functionality
   - Security controls for artifact execution (click-to-run in strict mode)
   - Improved dialog and response handling
   - Better error management for tool execution

5. Security Improvements:
   - Sandbox controls for artifact execution
   - Public/private artifact sharing controls
   - Security settings to control artifact behavior
   - CSP and frame-options handling for artifacts

6. Technical Improvements:
   - Better post streaming implementation
   - Improved error handling in completions
   - Better memory management for partial tool calls
   - Enhanced testing coverage

7. Configuration:
   - New site settings for artifact security
   - Extended LLM model configurations
   - Additional tool configuration options

This PR significantly enhances the plugin's capabilities for generating and displaying interactive content while maintaining security and providing flexible configuration options for administrators.
2024-11-19 09:22:39 +11:00

214 lines
5.5 KiB
Ruby

# frozen_string_literal: true
# This class can be used to process a stream of text that may contain XML tool
# calls.
# It will return either text or ToolCall objects.
module DiscourseAi
module Completions
class XmlToolProcessor
def initialize(partial_tool_calls: false)
@buffer = +""
@function_buffer = +""
@should_cancel = false
@in_tool = false
@partial_tool_calls = partial_tool_calls
@partial_tools = [] if @partial_tool_calls
end
def <<(text)
@buffer << text
result = []
if !@in_tool
# double check if we are clearly in a tool
search_length = text.length + 20
search_string = @buffer[-search_length..-1] || @buffer
index = search_string.rindex("<function_calls>")
@in_tool = !!index
if @in_tool
@function_buffer = @buffer[index..-1]
text_index = text.rindex("<function_calls>")
result << text[0..text_index - 1].rstrip if text_index && text_index > 0
end
else
add_to_function_buffer(text)
end
if !@in_tool
if maybe_has_tool?(@buffer)
split_index = text.rindex("<").to_i - 1
if split_index >= 0
@function_buffer = text[split_index + 1..-1] || ""
text = text[0..split_index] || ""
else
add_to_function_buffer(text)
text = ""
end
else
if @function_buffer.length > 0
result << @function_buffer
@function_buffer = +""
end
end
result << text if text.length > 0
else
@should_cancel = true if text.include?("</function_calls>")
end
if @should_notify_partial_tool
@should_notify_partial_tool = false
result << @partial_tools.last
end
result
end
def finish
return [] if @function_buffer.blank?
idx = -1
parse_malformed_xml(@function_buffer).map do |tool|
ToolCall.new(
id: "tool_#{idx += 1}",
name: tool[:tool_name],
parameters: tool[:parameters],
)
end
end
def should_cancel?
@should_cancel
end
private
def add_to_function_buffer(text)
@function_buffer << text
detect_partial_tool_calls(@function_buffer, text) if @partial_tool_calls
end
def detect_partial_tool_calls(buffer, delta)
parse_partial_tool_call(buffer)
end
def parse_partial_tool_call(buffer)
match =
buffer
.scan(
%r{
<invoke>
\s*
<tool_name>
([^<]+)
</tool_name>
\s*
<parameters>
(.*?)
(</parameters>|\Z)
}mx,
)
.to_a
.last
if match
params = partial_parse_params(match[1])
if params.present?
current_tool = @partial_tools.last
if !current_tool || current_tool.name != match[0].strip
current_tool =
ToolCall.new(
id: "tool_#{@partial_tools.length}",
name: match[0].strip,
parameters: params,
)
@partial_tools << current_tool
current_tool.partial = true
@should_notify_partial_tool = true
end
if current_tool.parameters != params
current_tool.parameters = params
@should_notify_partial_tool = true
end
end
end
end
def partial_parse_params(params)
params
.scan(%r{
<([^>]+)>
(.*?)
(</\1>|\Z)
}mx)
.each_with_object({}) do |(name, value), hash|
next if "<![CDATA[".start_with?(value)
hash[name.to_sym] = value.gsub(/^<!\[CDATA\[|\]\]>$/, "")
end
end
def parse_malformed_xml(input)
input
.scan(
%r{
<invoke>
\s*
<tool_name>
([^<]+)
</tool_name>
\s*
<parameters>
(.*?)
</parameters>
\s*
</invoke>
}mx,
)
.map do |tool_name, params|
{
tool_name: tool_name.strip,
parameters:
params
.scan(%r{
<([^>]+)>
(.*?)
</\1>
}mx)
.each_with_object({}) do |(name, value), hash|
hash[name.to_sym] = value.gsub(/^<!\[CDATA\[|\]\]>$/, "")
end,
}
end
end
def normalize_function_ids!(function_buffer)
function_buffer
.css("invoke")
.each_with_index do |invoke, index|
if invoke.at("tool_id")
invoke.at("tool_id").content = "tool_#{index}" if invoke.at("tool_id").content.blank?
else
invoke.add_child("<tool_id>tool_#{index}</tool_id>\n") if !invoke.at("tool_id")
end
end
end
def maybe_has_tool?(text)
# 16 is the length of function calls
substring = text[-16..-1] || text
split = substring.split("<")
if split.length > 1
match = "<" + split.last
"<function_calls>".start_with?(match)
else
substring.ends_with?("<")
end
end
end
end
end