Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image generation with Dall-e #398

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/jobs/get_next_ai_message_job.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'open-uri'
include ActionView::RecordIdentifier
require "nokogiri/xml/node"

Expand Down Expand Up @@ -190,6 +191,7 @@ def call_tools_before_wrapping_up
end

index = @message.index
url_of_dalle_generated_image = nil
msgs.each do |tool_message| # one message for each tool executed
@conversation.messages.create!(
assistant: @assistant,
Expand All @@ -200,6 +202,13 @@ def call_tools_before_wrapping_up
index: index += 1,
processed_at: Time.current,
)

parsed = JSON.parse(tool_message[:content]) rescue nil

if parsed.is_a?(Hash) && parsed.has_key?("url_of_dalle_generated_image")
url_of_dalle_generated_image = parsed["url_of_dalle_generated_image"]
end

end

assistant_reply = @conversation.messages.create!(
Expand All @@ -210,6 +219,12 @@ def call_tools_before_wrapping_up
index: index += 1
)

unless url_of_dalle_generated_image.nil?
d = Document.new
d.file.attach(io: URI.open(url_of_dalle_generated_image), filename: 'image.png')
assistant_reply.documents << d
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should assume that @message.content_tool_calls can have multiple Dalle toolbox calls. This means that within msgs.each do |tool_message| each of those calls will set the url_of_dalle_generated_image overwriting the previous.

So let's collect multiple url_of_dalle_generated_image into an array so that in this block here we push multiple documents into the assistant_reply.


GetNextAIMessageJob.perform_later(
@user.id,
assistant_reply.id,
Expand Down
1 change: 1 addition & 0 deletions app/services/toolbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def self.descendants
[
test_env && Toolbox::HelloWorld,
Toolbox::OpenMeteo,
Toolbox::Dalle,
Toolbox::Memory,
gmail_active && Toolbox::Gmail,
tasks_active && Toolbox::GoogleTasks,
Expand Down
36 changes: 36 additions & 0 deletions app/services/toolbox/dalle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class Toolbox::Dalle < Toolbox

describe :generate_an_image, <<~S
Generate an image based on what the user asks you to generate. You will pass the user's prompt and will get back a URL to an image.
krschacht marked this conversation as resolved.
Show resolved Hide resolved
S

def self.generate_an_image(image_generation_prompt_s:)

response = client.images.generate(
parameters: {
prompt: image_generation_prompt_s,
model: "dall-e-3",
size: "1024x1792",
quality: "standard"
}
)

dalle_url = response.dig("data", 0, "url")

{
prompt_given: image_generation_prompt_s,
url_of_dalle_generated_image: dalle_url,
note_to_assistant: "The image at the URL is already being shown on screen so reply with a nice message confirming the image has been generated, maybe re-describing it, but don't include the link to it."
}
end
ceicke marked this conversation as resolved.
Show resolved Hide resolved

class << self
private

def client
OpenAI::Client.new(
access_token: Current.user.openai_key,
)
end
end
end
2 changes: 1 addition & 1 deletion lib/active_storage/service/postgresql_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def generate_url(key, expires_in:, filename:, disposition:, content_type:)
)

generated_url = url_helpers.rails_postgresql_service_url(verified_key_with_expiration,
**url_options,
only_path: true, # This fixes an exception with attachment URL generation from a worker: https://github.com/AllYourBot/hostedgpt/pull/398#issuecomment-2168135853
disposition: content_disposition,
content_type: content_type,
filename: filename,
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/conversations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ weather:
title: Weather
last_assistant_message: weather_explained

image_generation:
user: christoph
assistant: samantha
title: Generating an image
last_assistant_message: image_generation_explained

trees:
user: keith
assistant: keith_gpt3
Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,45 @@ weather_explained:
version: 1


image_generation_tool_call:
assistant: samantha
conversation: image_generation
role: assistant
tool_call_id:
content_text:
content_tool_calls: '[{"index": 0, "id": "def456", "type": "function", "function": {"name": "generate_an_image", "arguments": {"image_generation_prompt_s": "World"}}}]'
content_document:
created_at: 2024-08-25 1:01:00
processed_at: 2024-08-25 1:01:00
index: 1
version: 1

image_generation_tool_result:
assistant: samantha
conversation: image_generation
role: tool
tool_call_id: def456
content_text: weather is
content_tool_calls:
content_document:
created_at: 2024-08-25 1:02:00
processed_at: 2024-08-25 1:02:00
index: 2
version: 1

image_generation_explained:
assistant: samantha
conversation: image_generation
role: assistant
tool_call_id:
content_text: The weather in Austin is
content_tool_calls:
content_document:
created_at: 2024-08-25 1:03:00
processed_at: 2024-08-25 1:03:00
index: 3
version: 1

# Next conversation

trees_explained:
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ taylor:
first_name: Taylor
registered_at: 2024-05-31 08:40:05
preferences: {}

christoph:
first_name: Christoph
registered_at: 2024-08-25 08:08:08
preferences: {}
59 changes: 57 additions & 2 deletions test/jobs/get_next_ai_message_job_openai_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
class GetNextAIMessageJobOpenaiTest < ActiveJob::TestCase
setup do
@conversation = conversations(:greeting)
@user = @conversation.user
@conversation.messages.create! role: :user, content_text: "Still there?", assistant: @conversation.assistant
@message = @conversation.latest_message_for_version(:latest)
@test_client = TestClient::OpenAI.new(access_token: "abc")

p "#########"
@image_generation = conversations(:image_generation)
p @image_generation
p @image_generation.messages.create! role: :user, content_text: "Generate an image", assistant: @image_generation.assistant
p @image_generation.messages.length
@image_generation_message = @image_generation.latest_message_for_version(:latest)
p @image_generation_message

@user = @conversation.user
@test_client = TestClient::OpenAI.new(access_token: 'abc')
end

test "populates the latest message from the assistant" do
Expand Down Expand Up @@ -56,6 +65,52 @@ class GetNextAIMessageJobOpenaiTest < ActiveJob::TestCase
refute second_new_message.finished?, "This message SHOULD NOT be considered finished yet"
end

test "properly handles a tool response call from the assistant when images are included" do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krschacht I tried to implement a test for the job, but I cannot get it to work. It already fails to add 2 new messages. Maybe I setup something wrong in the fixtures or in the setup...

assert_difference "@image_generation.messages.reload.length", 2 do
TestClient::OpenAI.stub :function, "generate_an_image" do
TestClient::OpenAI.stub :arguments, { image_generation_prompt_s: "Kitten" } do
TestClient::OpenAI.stub :api_response, TestClient::OpenAI.api_function_response do
assert GetNextAIMessageJob.perform_now(@user.id, @message.id, @image_generation.assistant.id)
end
end
end
end

p "###### Message:"
p @image_generation_message

@image_generation_message.reload
assert @image_generation_message.content_text.blank?
assert @image_generation_message.tool_call_id.nil?
assert @image_generation_message.content_tool_calls.present?, "Assistant should have decided to call a tool"

@new_messages = @image_generation.messages.where("id > ?", @message.id).order(:created_at)

p "###### Image generation:"
p @image_generation
@new_messages.messages.each_with_index do |message, index|
p "######## #{index}:"
p message
end

# first
first_new_message = @new_messages.first
assert first_new_message.tool?
# assert_equal "Hello, Keith!".to_json, first_new_message.content_text, "First new message should have the result of calling the tool"
# assert first_new_message.tool_call_id.present?
# assert first_new_message.content_tool_calls.blank?
# assert_equal @message.content_tool_calls.dig(0, :id), first_new_message.tool_call_id, "ID of tool execution should have matched decision to call the tool"
# assert first_new_message.finished?, "This message SHOULD HAVE been considered finished"

# second
# second_new_message = @new_messages.second
# assert second_new_message.assistant?, "Second new message should be queued up for the assistant to reply"
# assert second_new_message.content_text.nil?, "The content should be nil to indicate that it hasn't even started processing"
# assert second_new_message.tool_call_id.nil?
# assert second_new_message.content_tool_calls.blank?
# refute second_new_message.finished?, "This message SHOULD NOT be considered finished yet"
end

test "returns early if the message id was invalid" do
refute GetNextAIMessageJob.perform_now(@user.id, 0, @conversation.assistant.id)
end
Expand Down
2 changes: 1 addition & 1 deletion test/lib/active_storage/postgresql_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ActiveStorage::PostgresqlTest < ActiveSupport::TestCase
end

test "url generation" do
assert_match(/^https:\/\/example.com\/rails\/active_storage\/postgresql\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
assert_match(/^\/rails\/active_storage\/postgresql\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
@service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
end

Expand Down
2 changes: 1 addition & 1 deletion test/lib/active_storage/service/public_postgresql_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ class ActiveStorage::Service::PublicPostgresqlTest < ActiveSupport::TestCase
test "public URL generation" do
url = @service.url(@key, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")

assert_match(/^https:\/\/example.com\/rails\/active_storage\/postgresql\/.*\/avatar\.png/, url)
assert_match(/^\/rails\/active_storage\/postgresql\/.*\/avatar\.png/, url)
end
end
5 changes: 2 additions & 3 deletions test/models/document_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ class DocumentTest < ActiveSupport::TestCase
end

test "fully_processed_url" do
assert documents(:cat_photo).fully_processed_url(:small).starts_with?("http")
assert documents(:cat_photo).fully_processed_url(:small).include?("rails/active_storage/postgresql")
assert documents(:cat_photo).fully_processed_url(:small).exclude?("/redirect")
assert documents(:cat_photo).fully_processed_url(:small).include?('rails/active_storage/postgresql')
assert documents(:cat_photo).fully_processed_url(:small).exclude?('/redirect')
end

test "redirect_to_processed_path" do
Expand Down
Loading