capacitor-apple-intelligence 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapacitorAppleIntelligence.podspec +13 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +346 -0
- package/dist/esm/definitions.d.ts +223 -0
- package/dist/esm/definitions.js +10 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +33 -0
- package/dist/esm/index.js +36 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +12 -0
- package/dist/esm/web.js +47 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +90 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +93 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/AppleIntelligencePlugin/AppleIntelligence.swift +594 -0
- package/ios/Sources/AppleIntelligencePlugin/AppleIntelligencePlugin.swift +231 -0
- package/ios/Tests/AppleIntelligencePluginTests/AppleIntelligencePluginTests.swift +49 -0
- package/package.json +80 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
#if canImport(FoundationModels)
|
|
4
|
+
import FoundationModels
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
// MARK: - Error Types
|
|
8
|
+
|
|
9
|
+
/// Error codes for Apple Intelligence plugin
|
|
10
|
+
public enum AppleIntelligenceErrorCode: String {
|
|
11
|
+
case invalidJson = "INVALID_JSON"
|
|
12
|
+
case schemaMismatch = "SCHEMA_MISMATCH"
|
|
13
|
+
case unavailable = "UNAVAILABLE"
|
|
14
|
+
case nativeError = "NATIVE_ERROR"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Custom error type for Apple Intelligence operations
|
|
18
|
+
public struct AppleIntelligenceError: Error {
|
|
19
|
+
public let code: AppleIntelligenceErrorCode
|
|
20
|
+
public let message: String
|
|
21
|
+
|
|
22
|
+
public init(code: AppleIntelligenceErrorCode, message: String) {
|
|
23
|
+
self.code = code
|
|
24
|
+
self.message = message
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public var asDictionary: [String: Any] {
|
|
28
|
+
return [
|
|
29
|
+
"code": code.rawValue,
|
|
30
|
+
"message": message
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Message Types
|
|
36
|
+
|
|
37
|
+
/// Role for a message in the conversation
|
|
38
|
+
public enum MessageRole: String {
|
|
39
|
+
case system
|
|
40
|
+
case user
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// A message in the conversation
|
|
44
|
+
public struct Message {
|
|
45
|
+
public let role: MessageRole
|
|
46
|
+
public let content: String
|
|
47
|
+
|
|
48
|
+
public init(role: MessageRole, content: String) {
|
|
49
|
+
self.role = role
|
|
50
|
+
self.content = content
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Main Implementation
|
|
55
|
+
|
|
56
|
+
/// Apple Intelligence implementation class
|
|
57
|
+
/// Handles on-device LLM generation with JSON schema validation
|
|
58
|
+
@objc public class AppleIntelligence: NSObject {
|
|
59
|
+
|
|
60
|
+
// MARK: - Constants
|
|
61
|
+
|
|
62
|
+
private let maxRetries = 1
|
|
63
|
+
|
|
64
|
+
// MARK: - JSON Schema Validation
|
|
65
|
+
|
|
66
|
+
/// Validate JSON data against a JSON schema
|
|
67
|
+
private func validateAgainstSchema(_ json: Any, schema: [String: Any]) -> (valid: Bool, error: String?) {
|
|
68
|
+
guard let schemaType = schema["type"] as? String else {
|
|
69
|
+
return (false, "Schema missing 'type' property")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch schemaType {
|
|
73
|
+
case "object":
|
|
74
|
+
return validateObject(json, schema: schema)
|
|
75
|
+
case "array":
|
|
76
|
+
return validateArray(json, schema: schema)
|
|
77
|
+
case "string":
|
|
78
|
+
return (json is String, json is String ? nil : "Expected string, got \(type(of: json))")
|
|
79
|
+
case "number", "integer":
|
|
80
|
+
return (json is NSNumber && !(json is Bool),
|
|
81
|
+
(json is NSNumber && !(json is Bool)) ? nil : "Expected number, got \(type(of: json))")
|
|
82
|
+
case "boolean":
|
|
83
|
+
return (json is Bool, json is Bool ? nil : "Expected boolean, got \(type(of: json))")
|
|
84
|
+
case "null":
|
|
85
|
+
return (json is NSNull, json is NSNull ? nil : "Expected null, got \(type(of: json))")
|
|
86
|
+
default:
|
|
87
|
+
return (false, "Unknown schema type: \(schemaType)")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Validate an object against a schema
|
|
92
|
+
private func validateObject(_ json: Any, schema: [String: Any]) -> (valid: Bool, error: String?) {
|
|
93
|
+
guard let jsonObject = json as? [String: Any] else {
|
|
94
|
+
return (false, "Expected object, got \(type(of: json))")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check required properties
|
|
98
|
+
if let required = schema["required"] as? [String] {
|
|
99
|
+
for requiredProp in required {
|
|
100
|
+
if jsonObject[requiredProp] == nil {
|
|
101
|
+
return (false, "Missing required property: '\(requiredProp)'")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate properties against their schemas
|
|
107
|
+
if let properties = schema["properties"] as? [String: Any] {
|
|
108
|
+
for (key, value) in jsonObject {
|
|
109
|
+
if let propSchema = properties[key] as? [String: Any] {
|
|
110
|
+
let result = validateAgainstSchema(value, schema: propSchema)
|
|
111
|
+
if !result.valid {
|
|
112
|
+
return (false, "Property '\(key)': \(result.error ?? "validation failed")")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (true, nil)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Validate an array against a schema
|
|
122
|
+
private func validateArray(_ json: Any, schema: [String: Any]) -> (valid: Bool, error: String?) {
|
|
123
|
+
guard let jsonArray = json as? [Any] else {
|
|
124
|
+
return (false, "Expected array, got \(type(of: json))")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate items against item schema
|
|
128
|
+
if let itemSchema = schema["items"] as? [String: Any] {
|
|
129
|
+
for (index, item) in jsonArray.enumerated() {
|
|
130
|
+
let result = validateAgainstSchema(item, schema: itemSchema)
|
|
131
|
+
if !result.valid {
|
|
132
|
+
return (false, "Item at index \(index): \(result.error ?? "validation failed")")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate minItems
|
|
138
|
+
if let minItems = schema["minItems"] as? Int {
|
|
139
|
+
if jsonArray.count < minItems {
|
|
140
|
+
return (false, "Array has \(jsonArray.count) items, minimum is \(minItems)")
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate maxItems
|
|
145
|
+
if let maxItems = schema["maxItems"] as? Int {
|
|
146
|
+
if jsonArray.count > maxItems {
|
|
147
|
+
return (false, "Array has \(jsonArray.count) items, maximum is \(maxItems)")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (true, nil)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MARK: - Prompt Building
|
|
155
|
+
|
|
156
|
+
/// Build the system prompt with JSON schema instructions
|
|
157
|
+
private func buildSystemPrompt(userSystemPrompt: String?, schema: [String: Any]) -> String {
|
|
158
|
+
let schemaJson: String
|
|
159
|
+
do {
|
|
160
|
+
let schemaData = try JSONSerialization.data(withJSONObject: schema, options: [.prettyPrinted, .sortedKeys])
|
|
161
|
+
schemaJson = String(data: schemaData, encoding: .utf8) ?? "{}"
|
|
162
|
+
} catch {
|
|
163
|
+
schemaJson = "{}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var prompt = """
|
|
167
|
+
You are a JSON generator. Your response must be ONLY valid JSON that matches the provided schema.
|
|
168
|
+
|
|
169
|
+
SCHEMA:
|
|
170
|
+
\(schemaJson)
|
|
171
|
+
|
|
172
|
+
RULES:
|
|
173
|
+
1. Return ONLY the JSON object or array - nothing else
|
|
174
|
+
2. Do NOT wrap the response in markdown code blocks (no ```)
|
|
175
|
+
3. Do NOT include any comments
|
|
176
|
+
4. Do NOT include any explanations before or after the JSON
|
|
177
|
+
5. The JSON must be valid and parseable
|
|
178
|
+
6. All required properties must be present
|
|
179
|
+
7. Property types must match the schema exactly
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
if let userSystem = userSystemPrompt, !userSystem.isEmpty {
|
|
184
|
+
prompt += "\nADDITIONAL CONTEXT:\n\(userSystem)\n"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return prompt
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Build corrective prompt for retry attempts
|
|
191
|
+
private func buildCorrectivePrompt(previousResponse: String, validationError: String, schema: [String: Any]) -> String {
|
|
192
|
+
let schemaJson: String
|
|
193
|
+
do {
|
|
194
|
+
let schemaData = try JSONSerialization.data(withJSONObject: schema, options: [.prettyPrinted, .sortedKeys])
|
|
195
|
+
schemaJson = String(data: schemaData, encoding: .utf8) ?? "{}"
|
|
196
|
+
} catch {
|
|
197
|
+
schemaJson = "{}"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return """
|
|
201
|
+
The previous response was invalid JSON or did not match the required schema.
|
|
202
|
+
|
|
203
|
+
PREVIOUS RESPONSE:
|
|
204
|
+
\(previousResponse)
|
|
205
|
+
|
|
206
|
+
ERROR:
|
|
207
|
+
\(validationError)
|
|
208
|
+
|
|
209
|
+
REQUIRED SCHEMA:
|
|
210
|
+
\(schemaJson)
|
|
211
|
+
|
|
212
|
+
Fix the response and return ONLY valid JSON matching the schema. No explanations, no markdown, just the JSON.
|
|
213
|
+
"""
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// MARK: - Availability Check
|
|
217
|
+
|
|
218
|
+
/// Check if Apple Intelligence is available on this device
|
|
219
|
+
public func checkAvailability() -> (available: Bool, error: AppleIntelligenceError?) {
|
|
220
|
+
// Runtime check for iOS 26+
|
|
221
|
+
if #available(iOS 26, *) {
|
|
222
|
+
// Foundation Models framework is available
|
|
223
|
+
// Additional runtime check for Apple Intelligence capability would go here
|
|
224
|
+
return (true, nil)
|
|
225
|
+
} else {
|
|
226
|
+
return (false, AppleIntelligenceError(
|
|
227
|
+
code: .unavailable,
|
|
228
|
+
message: "Apple Intelligence requires iOS 26 or later. Current device is running an earlier version."
|
|
229
|
+
))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// MARK: - Generation
|
|
234
|
+
|
|
235
|
+
/// Parse raw text response as JSON
|
|
236
|
+
private func parseJsonResponse(_ text: String) -> Result<Any, AppleIntelligenceError> {
|
|
237
|
+
// Clean the response - remove any markdown code blocks if present
|
|
238
|
+
var cleanedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
239
|
+
|
|
240
|
+
// Remove markdown code blocks if they exist
|
|
241
|
+
if cleanedText.hasPrefix("```json") {
|
|
242
|
+
cleanedText = String(cleanedText.dropFirst(7))
|
|
243
|
+
} else if cleanedText.hasPrefix("```") {
|
|
244
|
+
cleanedText = String(cleanedText.dropFirst(3))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if cleanedText.hasSuffix("```") {
|
|
248
|
+
cleanedText = String(cleanedText.dropLast(3))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
cleanedText = cleanedText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
252
|
+
|
|
253
|
+
guard let data = cleanedText.data(using: .utf8) else {
|
|
254
|
+
return .failure(AppleIntelligenceError(
|
|
255
|
+
code: .invalidJson,
|
|
256
|
+
message: "Failed to convert response to UTF-8 data"
|
|
257
|
+
))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
do {
|
|
261
|
+
let json = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
|
|
262
|
+
return .success(json)
|
|
263
|
+
} catch {
|
|
264
|
+
return .failure(AppleIntelligenceError(
|
|
265
|
+
code: .invalidJson,
|
|
266
|
+
message: "Invalid JSON: \(error.localizedDescription)"
|
|
267
|
+
))
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Generate structured JSON output using Apple Intelligence
|
|
272
|
+
/// - Parameters:
|
|
273
|
+
/// - messages: Array of conversation messages
|
|
274
|
+
/// - schema: JSON schema the output must conform to
|
|
275
|
+
/// - Returns: Result containing parsed JSON or error
|
|
276
|
+
@available(iOS 26, *)
|
|
277
|
+
public func generate(
|
|
278
|
+
messages: [Message],
|
|
279
|
+
schema: [String: Any]
|
|
280
|
+
) async -> Result<Any, AppleIntelligenceError> {
|
|
281
|
+
// Import Foundation Models at runtime
|
|
282
|
+
// Note: This uses the new Foundation Models framework available in iOS 26+
|
|
283
|
+
|
|
284
|
+
// Extract system and user messages
|
|
285
|
+
let systemMessages = messages.filter { $0.role == .system }.map { $0.content }
|
|
286
|
+
let userMessages = messages.filter { $0.role == .user }.map { $0.content }
|
|
287
|
+
|
|
288
|
+
let userSystemPrompt = systemMessages.joined(separator: "\n")
|
|
289
|
+
let userQuery = userMessages.joined(separator: "\n")
|
|
290
|
+
|
|
291
|
+
// Build the full system prompt with schema
|
|
292
|
+
let systemPrompt = buildSystemPrompt(userSystemPrompt: userSystemPrompt, schema: schema)
|
|
293
|
+
|
|
294
|
+
// First attempt
|
|
295
|
+
var lastResponse = ""
|
|
296
|
+
var lastError = ""
|
|
297
|
+
|
|
298
|
+
for attempt in 0...maxRetries {
|
|
299
|
+
do {
|
|
300
|
+
let response: String
|
|
301
|
+
|
|
302
|
+
if attempt == 0 {
|
|
303
|
+
response = try await callLanguageModel(
|
|
304
|
+
systemPrompt: systemPrompt,
|
|
305
|
+
userPrompt: userQuery
|
|
306
|
+
)
|
|
307
|
+
} else {
|
|
308
|
+
// Retry with corrective prompt
|
|
309
|
+
let correctivePrompt = buildCorrectivePrompt(
|
|
310
|
+
previousResponse: lastResponse,
|
|
311
|
+
validationError: lastError,
|
|
312
|
+
schema: schema
|
|
313
|
+
)
|
|
314
|
+
response = try await callLanguageModel(
|
|
315
|
+
systemPrompt: systemPrompt,
|
|
316
|
+
userPrompt: correctivePrompt
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
lastResponse = response
|
|
321
|
+
|
|
322
|
+
// Parse JSON
|
|
323
|
+
let parseResult = parseJsonResponse(response)
|
|
324
|
+
switch parseResult {
|
|
325
|
+
case .success(let json):
|
|
326
|
+
// Validate against schema
|
|
327
|
+
let validation = validateAgainstSchema(json, schema: schema)
|
|
328
|
+
if validation.valid {
|
|
329
|
+
return .success(json)
|
|
330
|
+
} else {
|
|
331
|
+
lastError = validation.error ?? "Schema validation failed"
|
|
332
|
+
if attempt == maxRetries {
|
|
333
|
+
return .failure(AppleIntelligenceError(
|
|
334
|
+
code: .schemaMismatch,
|
|
335
|
+
message: "Schema validation failed after \(maxRetries + 1) attempts: \(lastError)"
|
|
336
|
+
))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
case .failure(let error):
|
|
340
|
+
lastError = error.message
|
|
341
|
+
if attempt == maxRetries {
|
|
342
|
+
return .failure(error)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
return .failure(AppleIntelligenceError(
|
|
347
|
+
code: .nativeError,
|
|
348
|
+
message: "Generation failed: \(error.localizedDescription)"
|
|
349
|
+
))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return .failure(AppleIntelligenceError(
|
|
354
|
+
code: .nativeError,
|
|
355
|
+
message: "Generation failed after all retry attempts"
|
|
356
|
+
))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Call the on-device language model
|
|
360
|
+
/// - Parameters:
|
|
361
|
+
/// - systemPrompt: The system instructions
|
|
362
|
+
/// - userPrompt: The user query
|
|
363
|
+
/// - Returns: The model's text response
|
|
364
|
+
@available(iOS 26, *)
|
|
365
|
+
private func callLanguageModel(systemPrompt: String, userPrompt: String) async throws -> String {
|
|
366
|
+
#if canImport(FoundationModels)
|
|
367
|
+
// Create a language model session
|
|
368
|
+
let session = LanguageModelSession()
|
|
369
|
+
|
|
370
|
+
// Build the prompt combining system and user messages
|
|
371
|
+
let fullPrompt = """
|
|
372
|
+
\(systemPrompt)
|
|
373
|
+
|
|
374
|
+
USER REQUEST:
|
|
375
|
+
\(userPrompt)
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
// Get response from the model
|
|
379
|
+
let response = try await session.respond(to: fullPrompt)
|
|
380
|
+
return response.content
|
|
381
|
+
|
|
382
|
+
#else
|
|
383
|
+
// Fallback for development/testing when FoundationModels isn't available
|
|
384
|
+
throw AppleIntelligenceError(
|
|
385
|
+
code: .unavailable,
|
|
386
|
+
message: "FoundationModels framework not available"
|
|
387
|
+
)
|
|
388
|
+
#endif
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
/// Generate method that returns a dictionary suitable for Capacitor bridge
|
|
393
|
+
@available(iOS 26, *)
|
|
394
|
+
public func generateForBridge(
|
|
395
|
+
messages: [[String: String]],
|
|
396
|
+
schema: [String: Any]
|
|
397
|
+
) async -> [String: Any] {
|
|
398
|
+
// Convert raw dictionaries to Message objects
|
|
399
|
+
let parsedMessages = messages.compactMap { dict -> Message? in
|
|
400
|
+
guard let roleStr = dict["role"],
|
|
401
|
+
let content = dict["content"],
|
|
402
|
+
let role = MessageRole(rawValue: roleStr) else {
|
|
403
|
+
return nil
|
|
404
|
+
}
|
|
405
|
+
return Message(role: role, content: content)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if parsedMessages.isEmpty {
|
|
409
|
+
return [
|
|
410
|
+
"success": false,
|
|
411
|
+
"error": AppleIntelligenceError(
|
|
412
|
+
code: .nativeError,
|
|
413
|
+
message: "No valid messages provided"
|
|
414
|
+
).asDictionary
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let result = await generate(messages: parsedMessages, schema: schema)
|
|
419
|
+
|
|
420
|
+
switch result {
|
|
421
|
+
case .success(let data):
|
|
422
|
+
return [
|
|
423
|
+
"success": true,
|
|
424
|
+
"data": data
|
|
425
|
+
]
|
|
426
|
+
case .failure(let error):
|
|
427
|
+
return [
|
|
428
|
+
"success": false,
|
|
429
|
+
"error": error.asDictionary
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/// Generate plain text output using Apple Intelligence
|
|
434
|
+
/// - Parameters:
|
|
435
|
+
/// - messages: Array of conversation messages
|
|
436
|
+
/// - Returns: Result containing generated text or error
|
|
437
|
+
@available(iOS 26, *)
|
|
438
|
+
public func generateText(
|
|
439
|
+
messages: [Message]
|
|
440
|
+
) async -> Result<String, AppleIntelligenceError> {
|
|
441
|
+
let systemMessages = messages.filter { $0.role == .system }.map { $0.content }
|
|
442
|
+
let userMessages = messages.filter { $0.role == .user }.map { $0.content }
|
|
443
|
+
|
|
444
|
+
let systemPrompt = systemMessages.joined(separator: "\n")
|
|
445
|
+
let userQuery = userMessages.joined(separator: "\n")
|
|
446
|
+
|
|
447
|
+
do {
|
|
448
|
+
let response = try await callLanguageModel(
|
|
449
|
+
systemPrompt: systemPrompt,
|
|
450
|
+
userPrompt: userQuery
|
|
451
|
+
)
|
|
452
|
+
return .success(response)
|
|
453
|
+
} catch {
|
|
454
|
+
return .failure(AppleIntelligenceError(
|
|
455
|
+
code: .nativeError,
|
|
456
|
+
message: "Generation failed: \(error.localizedDescription)"
|
|
457
|
+
))
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// Generate plain text output with specific language
|
|
462
|
+
/// - Parameters:
|
|
463
|
+
/// - messages: Array of conversation messages
|
|
464
|
+
/// - language: Target language for the response
|
|
465
|
+
/// - Returns: Result containing generated text or error
|
|
466
|
+
@available(iOS 26, *)
|
|
467
|
+
public func generateTextWithLanguage(
|
|
468
|
+
messages: [Message],
|
|
469
|
+
language: String
|
|
470
|
+
) async -> Result<String, AppleIntelligenceError> {
|
|
471
|
+
let systemMessages = messages.filter { $0.role == .system }.map { $0.content }
|
|
472
|
+
let userMessages = messages.filter { $0.role == .user }.map { $0.content }
|
|
473
|
+
|
|
474
|
+
// Convert language code to full language name for better model understanding
|
|
475
|
+
let languageMap: [String: String] = [
|
|
476
|
+
"en": "English",
|
|
477
|
+
"es": "Spanish",
|
|
478
|
+
"fr": "French",
|
|
479
|
+
"de": "German",
|
|
480
|
+
"ja": "Japanese",
|
|
481
|
+
"zh": "Chinese",
|
|
482
|
+
"it": "Italian",
|
|
483
|
+
"pt": "Portuguese",
|
|
484
|
+
"ru": "Russian",
|
|
485
|
+
"ar": "Arabic",
|
|
486
|
+
"ko": "Korean"
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
let fullLanguageName = languageMap[language.lowercased()] ?? language
|
|
490
|
+
|
|
491
|
+
var systemPrompt = systemMessages.joined(separator: "\n")
|
|
492
|
+
// Append language instruction
|
|
493
|
+
if !systemPrompt.isEmpty {
|
|
494
|
+
systemPrompt += "\n\n"
|
|
495
|
+
}
|
|
496
|
+
systemPrompt += "IMPORTANT: You must respond ONLY in \(fullLanguageName). Do not use any other language."
|
|
497
|
+
|
|
498
|
+
let userQuery = userMessages.joined(separator: "\n")
|
|
499
|
+
|
|
500
|
+
do {
|
|
501
|
+
let response = try await callLanguageModel(
|
|
502
|
+
systemPrompt: systemPrompt,
|
|
503
|
+
userPrompt: userQuery
|
|
504
|
+
)
|
|
505
|
+
return .success(response)
|
|
506
|
+
} catch {
|
|
507
|
+
return .failure(AppleIntelligenceError(
|
|
508
|
+
code: .nativeError,
|
|
509
|
+
message: "Generation failed: \(error.localizedDescription)"
|
|
510
|
+
))
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// Generate text bridge helper
|
|
515
|
+
@available(iOS 26, *)
|
|
516
|
+
public func generateTextForBridge(
|
|
517
|
+
messages: [[String: String]]
|
|
518
|
+
) async -> [String: Any] {
|
|
519
|
+
let parsedMessages = messages.compactMap { dict -> Message? in
|
|
520
|
+
guard let roleStr = dict["role"],
|
|
521
|
+
let content = dict["content"],
|
|
522
|
+
let role = MessageRole(rawValue: roleStr) else {
|
|
523
|
+
return nil
|
|
524
|
+
}
|
|
525
|
+
return Message(role: role, content: content)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if parsedMessages.isEmpty {
|
|
529
|
+
return [
|
|
530
|
+
"success": false,
|
|
531
|
+
"error": AppleIntelligenceError(
|
|
532
|
+
code: .nativeError,
|
|
533
|
+
message: "No valid messages provided"
|
|
534
|
+
).asDictionary
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let result = await generateText(messages: parsedMessages)
|
|
539
|
+
|
|
540
|
+
switch result {
|
|
541
|
+
case .success(let content):
|
|
542
|
+
return [
|
|
543
|
+
"success": true,
|
|
544
|
+
"content": content
|
|
545
|
+
]
|
|
546
|
+
case .failure(let error):
|
|
547
|
+
return [
|
|
548
|
+
"success": false,
|
|
549
|
+
"error": error.asDictionary
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/// Generate text with language bridge helper
|
|
555
|
+
@available(iOS 26, *)
|
|
556
|
+
public func generateTextWithLanguageForBridge(
|
|
557
|
+
messages: [[String: String]],
|
|
558
|
+
language: String
|
|
559
|
+
) async -> [String: Any] {
|
|
560
|
+
let parsedMessages = messages.compactMap { dict -> Message? in
|
|
561
|
+
guard let roleStr = dict["role"],
|
|
562
|
+
let content = dict["content"],
|
|
563
|
+
let role = MessageRole(rawValue: roleStr) else {
|
|
564
|
+
return nil
|
|
565
|
+
}
|
|
566
|
+
return Message(role: role, content: content)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if parsedMessages.isEmpty {
|
|
570
|
+
return [
|
|
571
|
+
"success": false,
|
|
572
|
+
"error": AppleIntelligenceError(
|
|
573
|
+
code: .nativeError,
|
|
574
|
+
message: "No valid messages provided"
|
|
575
|
+
).asDictionary
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let result = await generateTextWithLanguage(messages: parsedMessages, language: language)
|
|
580
|
+
|
|
581
|
+
switch result {
|
|
582
|
+
case .success(let content):
|
|
583
|
+
return [
|
|
584
|
+
"success": true,
|
|
585
|
+
"content": content
|
|
586
|
+
]
|
|
587
|
+
case .failure(let error):
|
|
588
|
+
return [
|
|
589
|
+
"success": false,
|
|
590
|
+
"error": error.asDictionary
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|