11# frozen_string_literal: true
22
3- require_relative ' ../function'
3+ require_relative " ../function"
44
55module DiscourseChatbot
66 class PaintEditFunction < Function
7- TOKEN_COST = 1000000 # 1M tokens per request based on cost of dall-e-3 model vs gpt-4o-mini
7+ TOKEN_COST = 1_000_000 # 1M tokens per request based on cost of dall-e-3 model vs gpt-4o-mini
88
99 def name
10- ' paint_edit_picture'
10+ " paint_edit_picture"
1111 end
1212
1313 def description
@@ -16,12 +16,22 @@ def description
1616
1717 def parameters
1818 [
19- { name : "description" , type : String , description : I18n . t ( "chatbot.prompt.function.paint_edit.parameters.description" ) } ,
19+ {
20+ name : "description" ,
21+ type : String ,
22+ description : I18n . t ( "chatbot.prompt.function.paint_edit.parameters.description" ) ,
23+ } ,
24+ {
25+ name : "aspect_ratio" ,
26+ type : String ,
27+ enum : PaintFunction ::ASPECT_RATIO_OPTIONS ,
28+ description : I18n . t ( "chatbot.prompt.function.paint_edit.parameters.aspect_ratio" ) ,
29+ } ,
2030 ]
2131 end
2232
2333 def required
24- [ ' description' ]
34+ [ " description" ]
2535 end
2636
2737 def process ( args , opts )
@@ -31,39 +41,49 @@ def process(args, opts)
3141
3242 description = args [ parameters [ 0 ] [ :name ] ]
3343
34- client = OpenAI ::Client . new do |f |
35- f . response :logger , Logger . new ( $stdout) , bodies : true if SiteSetting . chatbot_enable_verbose_console_logging
36- if SiteSetting . chatbot_enable_verbose_rails_logging != "off"
37- case SiteSetting . chatbot_verbose_rails_logging_destination_level
44+ client =
45+ OpenAI ::Client . new do |f |
46+ if SiteSetting . chatbot_enable_verbose_console_logging
47+ f . response :logger , Logger . new ( $stdout) , bodies : true
48+ end
49+ if SiteSetting . chatbot_enable_verbose_rails_logging != "off"
50+ case SiteSetting . chatbot_verbose_rails_logging_destination_level
3851 when "warn"
3952 f . response :logger , Rails . logger , bodies : true , log_level : :warn
4053 else
4154 f . response :logger , Rails . logger , bodies : true , log_level : :info
55+ end
4256 end
4357 end
58+
59+ type = opts [ :type ]
60+
61+ last_image_upload =
62+ (
63+ if type == ::DiscourseChatbot ::POST
64+ last_post_image_upload ( opts [ :reply_to_message_or_post_id ] )
65+ else
66+ last_message_image_upload ( opts [ :reply_to_message_or_post_id ] )
67+ end
68+ )
69+
70+ if last_image_upload . nil?
71+ return (
72+ { answer : I18n . t ( "chatbot.prompt.function.paint_edit.no_image_error" ) , token_usage : 0 }
73+ )
4474 end
4575
46- size = "1536x1024"
76+ aspect_ratio = resolved_aspect_ratio ( args [ parameters [ 1 ] [ :name ] ] , last_image_upload )
77+ size =
78+ PaintFunction . size_for ( SiteSetting . chatbot_support_picture_creation_model , aspect_ratio )
4779 quality = "auto"
4880
4981 options = {
5082 model : SiteSetting . chatbot_support_picture_creation_model ,
5183 prompt : description ,
5284 size : size ,
5385 quality : quality ,
54- }
55-
56- type = opts [ :type ]
57-
58- last_image_upload = type == ::DiscourseChatbot ::POST ? last_post_image_upload ( opts [ :reply_to_message_or_post_id ] ) : last_message_image_upload ( opts [ :reply_to_message_or_post_id ] )
59-
60- return {
61- answer : I18n . t ( "chatbot.prompt.function.paint_edit.no_image_error" ) ,
62- token_usage : 0
63- } if last_image_upload . nil?
64-
65- file_path = path = Discourse . store . path_for ( last_image_upload )
66- base64_encoded_data = Base64 . strict_encode64 ( File . read ( file_path ) )
86+ }
6787
6888 file_path = Discourse . store . path_for ( last_image_upload )
6989 extension = last_image_upload . extension
@@ -73,16 +93,17 @@ def process(args, opts)
7393 f . binmode
7494 f . write ( File . binread ( file_path ) )
7595 f . rewind
76-
96+
7797 # Specify the file with MIME type
7898 upload_io = Faraday ::Multipart ::FilePart . new ( f , mime_type )
79-
99+
80100 options [ :image ] = upload_io
81-
82- response = client . images . edit ( parameters : options )
83-
84- f . close
85- f . unlink
101+ begin
102+ response = client . images . edit ( parameters : options )
103+ ensure
104+ f . close
105+ f . unlink
106+ end
86107
87108 if response . dig ( "error" )
88109 error_text = "ERROR when trying to call paint API: #{ response . dig ( "error" , "message" ) } "
@@ -91,32 +112,30 @@ def process(args, opts)
91112
92113 tokens_used = response . dig ( "usage" , "total_tokens" )
93114
94- artifacts = response . dig ( "data" )
95- . to_a
96- . map { |art | art [ "b64_json" ] }
115+ artifacts = response . dig ( "data" ) . to_a . map { |art | art [ "b64_json" ] }
97116
98117 bot_username = SiteSetting . chatbot_bot_user
99118 bot_user = ::User . find_by ( username : bot_username )
100119
101120 thumbnails = base64_to_image ( artifacts , description , bot_user . id )
102- short_url = thumbnails . first . short_url
103- markdown = ""
104-
105- {
106- answer : markdown ,
107- token_usage : tokens_used
108- }
121+ markdown =
122+ PaintFunction . markdown_for (
123+ upload : thumbnails . first ,
124+ description : description ,
125+ fallback_size : size ,
126+ )
127+
128+ { answer : markdown , token_usage : tokens_used }
109129 rescue => e
110130 Rails . logger . error ( "Chatbot: Error in paint edit function: #{ e } " )
111131 if e . respond_to? ( :response )
112132 status = e . response [ :status ]
113133 message = e . response [ :body ] [ "error" ] [ "message" ]
114- Rails . logger . error ( "Chatbot: There was a problem with Image call: status: #{ status } , message: #{ message } " )
134+ Rails . logger . error (
135+ "Chatbot: There was a problem with Image call: status: #{ status } , message: #{ message } " ,
136+ )
115137 end
116- {
117- answer : I18n . t ( "chatbot.prompt.function.paint_edit.error" ) ,
118- token_usage : TOKEN_COST
119- }
138+ { answer : I18n . t ( "chatbot.prompt.function.paint_edit.error" ) , token_usage : TOKEN_COST }
120139 end
121140 end
122141
@@ -130,34 +149,39 @@ def base64_to_image(artifacts, description, user_id)
130149 f . binmode
131150 f . write ( Base64 . decode64 ( art ) )
132151 f . rewind
133- upload = UploadCreator . new ( f , attribution ) . create_for ( user_id )
134- f . unlink
152+ begin
153+ upload = UploadCreator . new ( f , attribution ) . create_for ( user_id )
154+ ensure
155+ f . close
156+ f . unlink
157+ end
135158
136- UploadSerializer . new ( upload , root : false )
159+ upload
137160 end
138161 end
139162
163+ def resolved_aspect_ratio ( aspect_ratio , upload )
164+ return PaintFunction . normalized_aspect_ratio ( aspect_ratio ) if aspect_ratio . present?
165+
166+ PaintFunction . aspect_ratio_for_upload ( upload )
167+ end
168+
140169 def last_post_image_upload ( post_id )
141170 post_collection = ::DiscourseChatbot ::PostPromptUtils . collect_past_interactions ( post_id )
142171
143172 return nil if post_collection . empty?
144-
173+
145174 upload_id = post_collection . map ( &:image_upload_id ) . compact . max
146- return Upload . find_by ( id : upload_id )
147-
148- nil
175+ Upload . find_by ( id : upload_id )
149176 end
150177
151178 def last_message_image_upload ( message_id )
152- message_collection = ::DiscourseChatbot ::MessagePromptUtils . collect_past_interactions ( message_id )
179+ message_collection =
180+ ::DiscourseChatbot ::MessagePromptUtils . collect_past_interactions ( message_id )
153181 uploads = [ ]
154182
155183 message_collection . each do |cm |
156- cm . uploads . each do |ul |
157- if %w[ png webp jpg jpeg ] . include? ( ul . extension )
158- uploads << ul
159- end
160- end
184+ cm . uploads . each { |ul | uploads << ul if %w[ png webp jpg jpeg ] . include? ( ul . extension ) }
161185 end
162186
163187 return nil if uploads . empty?
0 commit comments