@@ -28,7 +28,14 @@ const (
2828func appendToLastUserMessage (messages []ChatRequestMessage , text string ) {
2929 for i := len (messages ) - 1 ; i >= 0 ; i -- {
3030 if messages [i ].Role == "user" {
31- messages [i ].Content += "\n \n " + text
31+ if len (messages [i ].ContentParts ) > 0 {
32+ messages [i ].ContentParts = append (messages [i ].ContentParts , ChatContentPart {
33+ Type : "text" ,
34+ Text : text ,
35+ })
36+ } else {
37+ messages [i ].Content += "\n \n " + text
38+ }
3239 break
3340 }
3441 }
@@ -167,6 +174,21 @@ func ConvertAIMessageToStoredChatMessage(aiMsg uctypes.AIMessage) (*StoredChatMe
167174 return nil , fmt .Errorf ("invalid AIMessage: %w" , err )
168175 }
169176
177+ hasImages := false
178+ for _ , part := range aiMsg .Parts {
179+ if strings .HasPrefix (part .MimeType , "image/" ) {
180+ hasImages = true
181+ break
182+ }
183+ }
184+
185+ if hasImages {
186+ return convertAIMessageMultimodal (aiMsg )
187+ }
188+ return convertAIMessageTextOnly (aiMsg )
189+ }
190+
191+ func convertAIMessageTextOnly (aiMsg uctypes.AIMessage ) (* StoredChatMessage , error ) {
170192 var textBuilder strings.Builder
171193 firstText := true
172194 for _ , part := range aiMsg .Parts {
@@ -213,6 +235,89 @@ func ConvertAIMessageToStoredChatMessage(aiMsg uctypes.AIMessage) (*StoredChatMe
213235 }, nil
214236}
215237
238+ func convertAIMessageMultimodal (aiMsg uctypes.AIMessage ) (* StoredChatMessage , error ) {
239+ var contentParts []ChatContentPart
240+ imageCount := 0
241+ imageFailCount := 0
242+
243+ for _ , part := range aiMsg .Parts {
244+ switch {
245+ case part .Type == uctypes .AIMessagePartTypeText :
246+ if part .Text != "" {
247+ contentParts = append (contentParts , ChatContentPart {
248+ Type : "text" ,
249+ Text : part .Text ,
250+ })
251+ }
252+
253+ case strings .HasPrefix (part .MimeType , "image/" ):
254+ imageCount ++
255+ imageUrl , err := aiutil .ExtractImageUrl (part .Data , part .URL , part .MimeType )
256+ if err != nil {
257+ imageFailCount ++
258+ log .Printf ("openaichat: error extracting image URL for %s: %v\n " , part .FileName , err )
259+ continue
260+ }
261+ contentParts = append (contentParts , ChatContentPart {
262+ Type : "image_url" ,
263+ ImageUrl : & ChatImageUrl {Url : imageUrl },
264+ FileName : part .FileName ,
265+ PreviewUrl : part .PreviewUrl ,
266+ MimeType : part .MimeType ,
267+ })
268+
269+ case part .MimeType == "text/plain" :
270+ textData , err := aiutil .ExtractTextData (part .Data , part .URL )
271+ if err != nil {
272+ log .Printf ("openaichat: error extracting text data for %s: %v\n " , part .FileName , err )
273+ continue
274+ }
275+ formattedText := aiutil .FormatAttachedTextFile (part .FileName , textData )
276+ if formattedText != "" {
277+ contentParts = append (contentParts , ChatContentPart {
278+ Type : "text" ,
279+ Text : formattedText ,
280+ })
281+ }
282+
283+ case part .MimeType == "directory" :
284+ if len (part .Data ) == 0 {
285+ log .Printf ("openaichat: directory listing part missing data for %s\n " , part .FileName )
286+ continue
287+ }
288+ formattedText := aiutil .FormatAttachedDirectoryListing (part .FileName , string (part .Data ))
289+ if formattedText != "" {
290+ contentParts = append (contentParts , ChatContentPart {
291+ Type : "text" ,
292+ Text : formattedText ,
293+ })
294+ }
295+
296+ case part .MimeType == "application/pdf" :
297+ log .Printf ("openaichat: PDF attachments are not supported by Chat Completions API, skipping %s\n " , part .FileName )
298+ continue
299+
300+ default :
301+ continue
302+ }
303+ }
304+
305+ if len (contentParts ) == 0 {
306+ if imageCount > 0 && imageFailCount == imageCount {
307+ return nil , fmt .Errorf ("all %d image conversions failed" , imageCount )
308+ }
309+ return nil , errors .New ("message has no valid content after processing all parts" )
310+ }
311+
312+ return & StoredChatMessage {
313+ MessageId : aiMsg .MessageId ,
314+ Message : ChatRequestMessage {
315+ Role : "user" ,
316+ ContentParts : contentParts ,
317+ },
318+ }, nil
319+ }
320+
216321// ConvertToolResultsToNativeChatMessage converts tool results to OpenAI tool messages
217322func ConvertToolResultsToNativeChatMessage (toolResults []uctypes.AIToolResult ) ([]uctypes.GenAIMessage , error ) {
218323 if len (toolResults ) == 0 {
@@ -261,8 +366,36 @@ func ConvertAIChatToUIChat(aiChat uctypes.AIChat) (*uctypes.UIChat, error) {
261366
262367 var parts []uctypes.UIMessagePart
263368
264- // Add text content if present
265- if chatMsg .Message .Content != "" {
369+ if len (chatMsg .Message .ContentParts ) > 0 {
370+ for _ , cp := range chatMsg .Message .ContentParts {
371+ switch cp .Type {
372+ case "text" :
373+ if found , part := aiutil .ConvertDataUserFile (cp .Text ); found {
374+ if part != nil {
375+ parts = append (parts , * part )
376+ }
377+ } else {
378+ parts = append (parts , uctypes.UIMessagePart {
379+ Type : "text" ,
380+ Text : cp .Text ,
381+ })
382+ }
383+ case "image_url" :
384+ mimeType := cp .MimeType
385+ if mimeType == "" {
386+ mimeType = "image/*"
387+ }
388+ parts = append (parts , uctypes.UIMessagePart {
389+ Type : "data-userfile" ,
390+ Data : uctypes.UIMessageDataUserFile {
391+ FileName : cp .FileName ,
392+ MimeType : mimeType ,
393+ PreviewUrl : cp .PreviewUrl ,
394+ },
395+ })
396+ }
397+ }
398+ } else if chatMsg .Message .Content != "" {
266399 parts = append (parts , uctypes.UIMessagePart {
267400 Type : "text" ,
268401 Text : chatMsg .Message .Content ,
0 commit comments