11import Foundation
2+ import NetworkingCore
23
34/// Generative content remote that uses merchant's API key for OpenAI instead of Jetpack AI
45///
@@ -13,7 +14,9 @@ public final class MerchantGenerativeContentRemote: GenerativeContentRemoteProto
1314 base: String ,
1415 feature: GenerativeContentRemoteFeature ,
1516 responseFormat: GenerativeContentRemoteResponseFormat ) async throws -> String {
16- let url = URL ( string: " https://api.openai.com/v1/chat/completions " ) !
17+ guard let url = URL ( string: " https://api.openai.com/v1/chat/completions " ) else {
18+ throw NSError ( domain: " Invalid URL " , code: 0 )
19+ }
1720 var request = URLRequest ( url: url)
1821 request. httpMethod = " POST "
1922 request. setValue ( " Bearer \( apiKey) " , forHTTPHeaderField: " Authorization " )
@@ -63,12 +66,136 @@ public final class MerchantGenerativeContentRemote: GenerativeContentRemoteProto
6366 weightUnit: String ? ,
6467 categories: [ ProductCategory ] ,
6568 tags: [ ProductTag ] ) async throws -> AIProduct {
66- // For now throw an error
67- throw MerchantGenerativeContentRemoteError . notImplemented
69+
70+ // Build input components
71+ var inputComponents = [ String ( format: AIRequestPrompts . inputComponents, keywords, tone) ]
72+
73+ // Name will be added only if `productName` is available
74+ if let productName = productName, !productName. isEmpty {
75+ inputComponents. insert ( String ( format: AIRequestPrompts . productNameTemplate, productName) , at: 1 )
76+ }
77+
78+ let input = inputComponents. joined ( separator: " \n " )
79+
80+ // Build JSON response format dictionary
81+ let jsonResponseFormatDict : [ String : Any ] = {
82+ let tagsPrompt : String = {
83+ guard !tags. isEmpty else {
84+ return AIRequestPrompts . defaultTagsPrompt
85+ }
86+ return String ( format: AIRequestPrompts . existingTagsPrompt, tags. map { $0. name } . joined ( separator: " , " ) )
87+ } ( )
88+
89+ let categoriesPrompt : String = {
90+ guard !categories. isEmpty else {
91+ return AIRequestPrompts . defaultCategoriesPrompt
92+ }
93+ return String ( format: AIRequestPrompts . existingCategoriesPrompt, categories. map { $0. name } . joined ( separator: " , " ) )
94+ } ( )
95+
96+ let shippingPrompt = {
97+ var dict = [ String: String] ( )
98+ if let weightUnit {
99+ dict [ " weight " ] = String ( format: AIRequestPrompts . weightPrompt, weightUnit)
100+ }
101+
102+ if let dimensionUnit {
103+ dict [ " length " ] = String ( format: AIRequestPrompts . lengthPrompt, dimensionUnit)
104+ dict [ " width " ] = String ( format: AIRequestPrompts . widthPrompt, dimensionUnit)
105+ dict [ " height " ] = String ( format: AIRequestPrompts . heightPrompt, dimensionUnit)
106+ }
107+ return dict
108+ } ( )
109+
110+ return [ " names " : String ( format: AIRequestPrompts . namesFormat, language) ,
111+ " descriptions " : String ( format: AIRequestPrompts . descriptionsFormat, tone, language) ,
112+ " short_descriptions " : String ( format: AIRequestPrompts . shortDescriptionsFormat, tone, language) ,
113+ " virtual " : AIRequestPrompts . virtualFormat,
114+ " shipping " : shippingPrompt,
115+ " price " : String ( format: AIRequestPrompts . priceFormat, currencySymbol) ,
116+ " tags " : tagsPrompt,
117+ " categories " : categoriesPrompt]
118+ } ( )
119+
120+ let expectedJsonFormat = String ( format: AIRequestPrompts . jsonFormatInstructions,
121+ jsonResponseFormatDict. toJSONEncoded ( ) ?? " " )
122+
123+ let prompt = input + " \n " + expectedJsonFormat
124+
125+ // Make OpenAI API request
126+ guard let url = URL ( string: " https://api.openai.com/v1/chat/completions " ) else {
127+ throw MerchantGenerativeContentRemoteError . invalidResponse
128+ }
129+
130+ var request = URLRequest ( url: url)
131+ request. httpMethod = " POST "
132+ request. setValue ( " Bearer \( apiKey) " , forHTTPHeaderField: " Authorization " )
133+ request. setValue ( " application/json " , forHTTPHeaderField: " Content-Type " )
134+
135+ let parameters : [ String : Any ] = [
136+ " model " : " gpt-4 " ,
137+ " messages " : [
138+ [
139+ " role " : " user " ,
140+ " content " : prompt
141+ ]
142+ ] ,
143+ // gpt-4 does not support response_format parameter with json_object type, which does work with gpt-3 and the Jetpack tunnel
144+ // This may change further based on the selected model, and the AI provider, so has to be handled here.
145+ // ie:
146+ // "response_format": ["type": "json_object"],
147+ " max_tokens " : 4000 ,
148+ " temperature " : 0.7
149+ ]
150+
151+ request. httpBody = try JSONSerialization . data ( withJSONObject: parameters)
152+
153+ let ( data, _) = try await URLSession . shared. data ( for: request)
154+
155+ // Parse OpenAI response
156+ guard let json = try JSONSerialization . jsonObject ( with: data) as? [ String : Any ] ,
157+ let choices = json [ " choices " ] as? [ [ String : Any ] ] ,
158+ let firstChoice = choices. first,
159+ let message = firstChoice [ " message " ] as? [ String : Any ] ,
160+ let content = message [ " content " ] as? String else {
161+ throw MerchantGenerativeContentRemoteError . invalidResponse
162+ }
163+
164+ // Parse the AI-generated product JSON
165+ guard let contentData = content. data ( using: . utf8) ,
166+ let productJson = try JSONSerialization . jsonObject ( with: contentData) as? [ String : Any ] else {
167+ throw MerchantGenerativeContentRemoteError . invalidResponse
168+ }
169+
170+ // Extract and create AIProduct manually
171+ let names = productJson [ " names " ] as? [ String ] ?? [ ]
172+ let descriptions = productJson [ " descriptions " ] as? [ String ] ?? [ ]
173+ let shortDescriptions = productJson [ " short_descriptions " ] as? [ String ] ?? [ ]
174+ let virtual = productJson [ " virtual " ] as? Bool ?? false
175+ let price = productJson [ " price " ] as? String ?? " "
176+ let aiTags = productJson [ " tags " ] as? [ String ] ?? [ ]
177+ let aiCategories = productJson [ " categories " ] as? [ String ] ?? [ ]
178+
179+ // Extract shipping info
180+ let shippingDict = productJson [ " shipping " ] as? [ String : Any ] ?? [ : ]
181+ let length = shippingDict [ " length " ] as? String ?? " "
182+ let weight = shippingDict [ " weight " ] as? String ?? " "
183+ let width = shippingDict [ " width " ] as? String ?? " "
184+ let height = shippingDict [ " height " ] as? String ?? " "
185+
186+ let shipping = AIProduct . Shipping ( length: length, weight: weight, width: width, height: height)
187+
188+ return AIProduct ( names: names,
189+ descriptions: descriptions,
190+ shortDescriptions: shortDescriptions,
191+ virtual: virtual,
192+ shipping: shipping,
193+ tags: aiTags,
194+ price: price,
195+ categories: aiCategories)
68196 }
69197}
70198
71199private enum MerchantGenerativeContentRemoteError : Error {
72- case notImplemented
73200 case invalidResponse
74201}
0 commit comments