Roman Rizzi f9d7d7f5f0
DEV: AI bot migration to the Llm pattern. (#343)
* DEV: AI bot migration to the Llm pattern.

We added tool and conversation context support to the Llm service in discourse-ai#366, meaning we met all the conditions to migrate this module.

This PR migrates to the new pattern, meaning adding a new bot now requires minimal effort as long as the service supports it. On top of this, we introduce the concept of a "Playground" to separate the PM-specific bits from the completion, allowing us to use the bot in other contexts like chat in the future. Commands are called tools, and we simplified all the placeholder logic to perform updates in a single place, making the flow more one-wayish.

* Followup fixes based on testing

* Cleanup unused inference code

* FIX: text-based tools could be in the middle of a sentence

* GPT-4-turbo support

* Use new LLM API
2024-01-04 10:44:07 -03:00

126 lines
3.2 KiB
Ruby

# frozen_string_literal: true
module DiscourseAi
module AiBot
module Tools
class DallE < Tool
def self.signature
{
name: name,
description: "Renders images from supplied descriptions",
parameters: [
{
name: "prompts",
description:
"The prompts used to generate or create or draw the image (5000 chars or less, be creative) up to 4 prompts",
type: "array",
item_type: "string",
required: true,
},
],
}
end
def self.name
"dall_e"
end
def prompts
parameters[:prompts]
end
def chain_next_response?
false
end
def invoke(bot_user, _llm)
# max 4 prompts
max_prompts = prompts.take(4)
progress = +""
yield(progress)
results = nil
# this ensures multisite safety since background threads
# generate the images
api_key = SiteSetting.ai_openai_api_key
api_url = SiteSetting.ai_openai_dall_e_3_url
threads = []
max_prompts.each_with_index do |prompt, index|
threads << Thread.new(prompt) do |inner_prompt|
attempts = 0
begin
DiscourseAi::Inference::OpenAiImageGenerator.perform!(
inner_prompt,
api_key: api_key,
api_url: api_url,
)
rescue => e
attempts += 1
sleep 2
retry if attempts < 3
Discourse.warn_exception(
e,
message: "Failed to generate image for prompt #{prompt}",
)
nil
end
end
end
while true
progress << "."
yield(progress)
break if threads.all? { |t| t.join(2) }
end
results = threads.filter_map(&:value)
if results.blank?
return { prompts: max_prompts, error: "Something went wrong, could not generate image" }
end
uploads = []
results.each_with_index do |result, index|
result[:data].each do |image|
Tempfile.create("v1_txt2img_#{index}.png") do |file|
file.binmode
file.write(Base64.decode64(image[:b64_json]))
file.rewind
uploads << {
prompt: image[:revised_prompt],
upload: UploadCreator.new(file, "image.png").create_for(bot_user.id),
}
end
end
end
self.custom_raw = <<~RAW
[grid]
#{
uploads
.map do |item|
"![#{item[:prompt].gsub(/\|\'\"/, "")}|512x512, 50%](#{item[:upload].short_url})"
end
.join(" ")
}
[/grid]
RAW
{ prompts: uploads.map { |item| item[:prompt] } }
end
protected
def description_args
{ prompt: prompts.first }
end
end
end
end
end