Skip to content

Commit 65f3d6e

Browse files
committed
duplicate generateAIProduct for merchant key and gpt-4 response
1 parent 39e4621 commit 65f3d6e

File tree

7 files changed

+172
-20
lines changed

7 files changed

+172
-20
lines changed

Modules/Sources/Networking/Remote/MerchantGenerativeContentRemote.swift

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 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

71199
private enum MerchantGenerativeContentRemoteError: Error {
72-
case notImplemented
73200
case invalidResponse
74201
}

Modules/Sources/Yosemite/Actions/ProductAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ public enum ProductAction: Action {
200200
weightUnit: String?,
201201
categories: [ProductCategory],
202202
tags: [ProductTag],
203+
AISource: AISource,
203204
completion: (Result<AIProduct, Error>) -> Void)
204205

205206
/// Fetches stock based on the given status for a site

Modules/Sources/Yosemite/Stores/ProductStore.swift

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public class ProductStore: Store {
148148
weightUnit,
149149
categories,
150150
tags,
151+
aiSource,
151152
completion):
152153
generateAIProduct(siteID: siteID,
153154
productName: productName,
@@ -159,6 +160,7 @@ public class ProductStore: Store {
159160
weightUnit: weightUnit,
160161
categories: categories,
161162
tags: tags,
163+
aiSource: aiSource,
162164
completion: completion)
163165
case let .fetchStockReport(siteID, stockType, pageNumber, pageSize, order, completion):
164166
fetchStockReport(siteID: siteID,
@@ -745,22 +747,41 @@ private extension ProductStore {
745747
weightUnit: String?,
746748
categories: [ProductCategory],
747749
tags: [ProductTag],
750+
aiSource: AISource,
748751
completion: @escaping (Result<AIProduct, Error>) -> Void) {
749752
Task { @MainActor in
750-
let result = await Result {
751-
let product = try await generativeContentRemote.generateAIProduct(siteID: siteID,
752-
productName: productName,
753-
keywords: keywords,
754-
language: language,
755-
tone: tone,
756-
currencySymbol: currencySymbol,
757-
dimensionUnit: dimensionUnit,
758-
weightUnit: weightUnit,
759-
categories: categories,
760-
tags: tags)
761-
return product
753+
switch aiSource {
754+
case .jetpack:
755+
let result = await Result {
756+
let product = try await generativeContentRemote.generateAIProduct(siteID: siteID,
757+
productName: productName,
758+
keywords: keywords,
759+
language: language,
760+
tone: tone,
761+
currencySymbol: currencySymbol,
762+
dimensionUnit: dimensionUnit,
763+
weightUnit: weightUnit,
764+
categories: categories,
765+
tags: tags)
766+
return product
767+
}
768+
completion(result)
769+
case .merchant:
770+
let result = await Result {
771+
let key = ProcessInfo.processInfo.environment["openai-hack-key"] ?? "api key not found"
772+
return try await MerchantGenerativeContentRemote(apiKey: key).generateAIProduct(siteID: siteID,
773+
productName: productName,
774+
keywords: keywords,
775+
language: language,
776+
tone: tone,
777+
currencySymbol: currencySymbol,
778+
dimensionUnit: dimensionUnit,
779+
weightUnit: weightUnit,
780+
categories: categories,
781+
tags: tags)
782+
}
783+
completion(result)
762784
}
763-
completion(result)
764785
}
765786
}
766787

WooCommerce/Classes/ViewRelated/Products/AI/ProductDescriptionGenerationViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ final class ProductDescriptionGenerationViewModel: ObservableObject {
4949
/// Language used in product identified by AI
5050
///
5151
private var languageIdentifiedUsingAI: String?
52-
52+
5353
private var aiSource: AISource {
5454
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack
5555
}

WooCommerce/Classes/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ final class ProductSharingMessageGenerationViewModel: ObservableObject {
5151
/// Language used in product identified by AI
5252
///
5353
private var languageIdentifiedUsingAI: String?
54-
54+
5555
private var aiSource: AISource {
5656
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack
5757
}

WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/EntryPoint/AddProductWithAIActionSheet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ struct AddProductWithAIActionSheet: View {
3939
self.onAIOption = onAIOption
4040
self.onProductTypeOption = onProductTypeOption
4141
}
42-
42+
4343
private var aiSource: AISource {
4444
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack
4545
}

WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/Preview/ProductDetailPreviewViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,13 +554,15 @@ private extension ProductDetailPreviewViewModel {
554554

555555
return try await generateAIProduct(language: language,
556556
tone: tone,
557+
aiSource: aiSource,
557558
existingCategories: existingCategories,
558559
existingTags: existingTags)
559560
}
560561

561562
@MainActor
562563
func generateAIProduct(language: String,
563564
tone: AIToneVoice,
565+
aiSource: AISource,
564566
existingCategories: [ProductCategory],
565567
existingTags: [ProductTag]) async throws -> AIProduct {
566568
try await withCheckedThrowingContinuation { continuation in
@@ -574,6 +576,7 @@ private extension ProductDetailPreviewViewModel {
574576
weightUnit: weightUnit,
575577
categories: existingCategories,
576578
tags: existingTags,
579+
AISource: aiSource,
577580
completion: { result in
578581
continuation.resume(with: result)
579582
}))

0 commit comments

Comments
 (0)