From 5e99b819142eeaf3745f9fa2f64d9a86bbdf809d Mon Sep 17 00:00:00 2001 From: Keith Schacht Date: Mon, 4 Nov 2024 19:26:47 -0600 Subject: [PATCH] Attach images to conversations as URLs (#424) Co-authored-by: Justin Vallelonga --- .github/workflows/rubyonrails.yml | 6 ++++++ Dockerfile | 4 ++-- Gemfile | 3 ++- app/models/document.rb | 20 +++++++++++++++++++ app/models/message.rb | 4 +--- app/models/message/document_image.rb | 17 +++------------- app/services/ai_backend/open_ai.rb | 2 +- app/views/messages/_message.html.erb | 8 ++++---- config/application.rb | 8 ++++++++ config/environments/development.rb | 3 ++- config/environments/production.rb | 1 + config/options.yml | 4 ++++ docker-compose.yml | 4 ++++ .../service/postgresql_service.rb | 11 +++++++--- test/models/message/document_image_test.rb | 10 +++++----- 15 files changed, 71 insertions(+), 34 deletions(-) diff --git a/.github/workflows/rubyonrails.yml b/.github/workflows/rubyonrails.yml index 9b9fe16f0..b4cbf6c66 100644 --- a/.github/workflows/rubyonrails.yml +++ b/.github/workflows/rubyonrails.yml @@ -28,6 +28,9 @@ jobs: env: RAILS_ENV: test DATABASE_URL: "postgres://rails:password@localhost:5432/hostedgpt_test" + APP_URL_PROTOCOL: "http" + APP_URL_HOST: "localhost" + APP_URL_PORT: "3000" steps: - name: Checkout code uses: actions/checkout@v4 @@ -68,6 +71,9 @@ jobs: env: RAILS_ENV: test DATABASE_URL: "postgres://rails:password@localhost:5432/hostedgpt_test" + APP_URL_PROTOCOL: "http" + APP_URL_HOST: "localhost" + APP_URL_PORT: "3000" DISPLAY: "=:99" CHROME_VERSION: "127.0.6533.119" diff --git a/Dockerfile b/Dockerfile index f00aedb32..8a3c68ba2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,7 @@ RUN bundle exec bootsnap precompile app/ lib/ RUN grep -l '#!/usr/bin/env ruby' /rails/bin/* | xargs sed -i '/^#!/aDir.chdir File.expand_path("..", __dir__)' # Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile +RUN SECRET_KEY_BASE_DUMMY=1 VALIDATE_ENV_VARS=0 ./bin/rails assets:precompile # Final stage for app image @@ -134,7 +134,7 @@ COPY . . # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN bundle exec bootsnap precompile --gemfile app/ lib/ -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile +RUN SECRET_KEY_BASE_DUMMY=1 VALIDATE_ENV_VARS=0 ./bin/rails assets:precompile RUN mkdir -p log tmp bin diff --git a/Gemfile b/Gemfile index 3013be528..7879c5ded 100644 --- a/Gemfile +++ b/Gemfile @@ -41,13 +41,14 @@ gem "ffi", "~> 1.15.5" # explicitly requiring 15.5 until this is resolved: https gem "amatch", "~> 0.4.1" # enables fuzzy comparison of strings, a tool uses this gem "rails_heroicon", "~> 2.2.0" gem "ruby-openai", "~> 7.0.1" -gem "anthropic", "~> 0.1.0" +gem "anthropic", "~> 0.1.0" # TODO update to the latest version gem "tiktoken_ruby", "~> 0.0.9" gem "solid_queue", "~> 1.0.0" gem "name_of_person" gem "actioncable-enhanced-postgresql-adapter" # longer paylaods w/ postgresql actioncable gem "aws-sdk-s3", require: false gem "postmark-rails" +gem "ostruct" gem "omniauth", "~> 2.1" gem "omniauth-google-oauth2", "~> 1.1" diff --git a/app/models/document.rb b/app/models/document.rb index 6fb523877..68ab2f5b9 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -19,6 +19,26 @@ class Document < ApplicationRecord validates :purpose, :filename, :bytes, presence: true validate :file_present + def has_image?(variant = nil) + if variant.present? + return has_file_variant_processed?(variant) + end + + file.attached? + end + + def image_url(variant, fallback: nil) + return nil unless has_image? + + if has_file_variant_processed?(variant) + fully_processed_url(variant) + elsif fallback.nil? + redirect_to_processed_path(variant) + else + fallback + end + end + def file_data_url(variant = :large) return nil if !file.attached? diff --git a/app/models/message.rb b/app/models/message.rb index 36148bedb..315b2b97e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -43,9 +43,7 @@ def finished? (content_text.present? || content_tool_calls.present?) end - def not_finished? - !finished? - end + def not_finished? = !finished? private diff --git a/app/models/message/document_image.rb b/app/models/message/document_image.rb index 0b6358c94..c778cea54 100644 --- a/app/models/message/document_image.rb +++ b/app/models/message/document_image.rb @@ -8,23 +8,12 @@ module Message::DocumentImage end def has_document_image?(variant = nil) - has_image = documents.present? && documents.first.file.attached? - if has_image && variant - has_image = documents.first.has_file_variant_processed?(variant) - end - - !!has_image + documents.present? && documents.first.has_image?(variant) end - def document_image_path(variant, fallback: nil) + def document_image_url(variant, fallback: nil) return nil unless has_document_image? - if documents.first.has_file_variant_processed?(variant) - documents.first.fully_processed_url(variant) - elsif fallback.nil? - documents.first.redirect_to_processed_path(variant) - else - fallback - end + documents.first.image_url(variant, fallback: fallback) end end diff --git a/app/services/ai_backend/open_ai.rb b/app/services/ai_backend/open_ai.rb index f00fc713f..3930a7d0e 100644 --- a/app/services/ai_backend/open_ai.rb +++ b/app/services/ai_backend/open_ai.rb @@ -98,7 +98,7 @@ def preceding_conversation_messages content_with_images = [{ type: "text", text: message.content_text }] content_with_images += message.documents.collect do |document| - { type: "image_url", image_url: { url: document.file_data_url(:large) }} + { type: "image_url", image_url: { url: document.image_url(:large) }} end { diff --git a/app/views/messages/_message.html.erb b/app/views/messages/_message.html.erb index 4ba6a954a..794d00101 100644 --- a/app/views/messages/_message.html.erb +++ b/app/views/messages/_message.html.erb @@ -82,10 +82,10 @@ end %> role: "image-preview", controller: "image-loader", image_loader_message_scroller_outlet: "[data-role='inner-message']", - image_loader_url_value: message.document_image_path(:small), + image_loader_url_value: message.document_image_url(:small), action: "modal#open", } do %> - <%= image_tag message.document_image_path(:small, fallback: ""), + <%= image_tag message.document_image_url(:small, fallback: ""), class: %| my-0 mx-auto @@ -254,10 +254,10 @@ end %> class="flex flex-col md:flex-row justify-center" data-controller="image-loader" data-image-loader-message-scroller-outlet="[data-role='inner-message']" - data-image-loader-url-value="<%= message.document_image_path(:large) %>" + data-image-loader-url-value="<%= message.document_image_url(:large) %>" data-turbo-permanent > - <%= image_tag message.document_image_path(:large, fallback: ""), + <%= image_tag message.document_image_url(:large, fallback: ""), class: "w-full h-auto", data: { image_loader_target: "image", diff --git a/config/application.rb b/config/application.rb index 4bc219189..2f53f0fa0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -37,6 +37,14 @@ class Application < Rails::Application config.time_zone = "Central Time (US & Canada)" config.eager_load_paths << Rails.root.join("lib") + if Setting.validate_env_vars == "1" + Setting.require_keys!(:app_url_protocol, :app_url_host, :app_url_port) + end + config.app_url_protocol = Setting.app_url_protocol + config.app_url_host = Setting.app_url_host + config.app_url_port = Setting.app_url_port + config.app_url = "#{Setting.app_url_protocol}://#{Setting.app_url_host}:#{Setting.app_url_port}" + # Active Storage if Feature.cloudflare_storage? config.active_storage.service = :cloudflare diff --git a/config/environments/development.rb b/config/environments/development.rb index 499c3c2ba..53b35c62f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -85,8 +85,9 @@ log_polling = ENV["SOLID_QUEUE_LOG_POLLING_ON"] != "false" config.solid_queue.silence_polling = log_polling # NOTE: this is backwards, true means silence - config.web_console.permissions = ["192.168.0.0/16", "172.17.0.0/16"] + config.web_console.permissions = ["192.168.0.0/16", "172.17.0.0/16", "172.18.0.0/16"] + config.hosts << Setting.app_url_host config.hosts << ENV["DEV_HOST"] if ENV["DEV_HOST"].present? stdout_logger = ActiveSupport::Logger.new(STDOUT) diff --git a/config/environments/production.rb b/config/environments/production.rb index a43f85e4c..f5f5c9b1b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -98,6 +98,7 @@ # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] config.hosts = ENV["PRODUCTION_HOST"].split(",").map(&:strip) if ENV["PRODUCTION_HOST"] + # config.hosts << Setting.app_url_host # Skip DNS rebinding protection for the default health check endpoint. config.host_authorization = { exclude: ->(request) { request.path == "/up" } } diff --git a/config/options.yml b/config/options.yml index ee007b39c..44ee2ebe8 100644 --- a/config/options.yml +++ b/config/options.yml @@ -47,6 +47,10 @@ shared: password_reset_email: <%= ENV["PASSWORD_RESET_EMAIL_FEATURE"] || default_to(false, except_env_test: true) %> settings: # Be sure to add these ENV to docker-compose.yml + app_url_protocol: <%= ENV["APP_URL_PROTOCOL"] %> + app_url_host: <%= ENV["APP_URL_HOST"] %> + app_url_port: <%= ENV["APP_URL_PORT"] %> + validate_env_vars: <%= ENV["VALIDATE_ENV_VARS"] || "1" %> product_name: <%= ENV["PRODUCT_NAME"] || "HostedGPT" %> default_openai_key: <%= ENV["DEFAULT_OPENAI_KEY"] %> default_anthropic_key: <%= ENV["DEFAULT_ANTHROPIC_KEY"] %> diff --git a/docker-compose.yml b/docker-compose.yml index 012612c85..28d6e1c26 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,10 @@ services: target: development environment: # Be sure to add environment variables to config/options.yml + - APP_URL_PROTOCOL + - APP_URL_HOST + - APP_URL_PORT + - VALIDATE_ENV_VARS - DATABASE_URL=postgres://app:secret@postgres/app_development - DEV_HOST=${DEV_HOST:-localhost} # Set if you want to use a different hostname - OVERMIND_COLORS=2,3,5 diff --git a/lib/active_storage/service/postgresql_service.rb b/lib/active_storage/service/postgresql_service.rb index e4d46646a..c906b0ef5 100644 --- a/lib/active_storage/service/postgresql_service.rb +++ b/lib/active_storage/service/postgresql_service.rb @@ -148,10 +148,15 @@ def url_helpers def url_options if ActiveStorage::Current.respond_to?(:url_options) - ActiveStorage::Current.url_options - else - { host: ActiveStorage::Current.host } + url_opts = ActiveStorage::Current.url_options + return url_opts if url_opts.is_a?(Hash) end + + return { + protocol: Rails.application.config.app_url_protocol, + host: Rails.application.config.app_url_host, + port: Rails.application.config.app_url_port, + } end end end diff --git a/test/models/message/document_image_test.rb b/test/models/message/document_image_test.rb index 1b0c8c675..0ceca2088 100644 --- a/test/models/message/document_image_test.rb +++ b/test/models/message/document_image_test.rb @@ -16,12 +16,12 @@ class Message::DocumentImageTest < ActiveSupport::TestCase refute messages(:examine_this).has_document_image?(:small) end - test "document_image_path" do - assert messages(:examine_this).document_image_path(:small).is_a?(String) - assert messages(:examine_this).document_image_path(:small).starts_with?("/rails/active_storage/representations/redirect") + test "document_image_url" do + assert messages(:examine_this).document_image_url(:small).is_a?(String) + assert messages(:examine_this).document_image_url(:small).starts_with?("/rails/active_storage/representations/redirect") end - test "document_image_path with fallback" do - assert_equal "", messages(:examine_this).document_image_path(:small, fallback: "") + test "document_image_url with fallback" do + assert_equal "", messages(:examine_this).document_image_url(:small, fallback: "") end end