@capgo/capacitor-llm 8.0.19 → 8.1.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/CapgoCapacitorLlm.podspec +1 -9
- package/Package.swift +21 -3
- package/README.md +137 -163
- package/android/build.gradle +3 -2
- package/android/src/main/java/ee/forgr/capgo_llm/LLM.java +605 -283
- package/android/src/main/java/ee/forgr/capgo_llm/LLMPlugin.java +36 -63
- package/dist/docs.json +66 -35
- package/dist/esm/definitions.d.ts +28 -20
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +68 -39
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +68 -39
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +68 -39
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LLMPlugin/LLMPlugin.swift +384 -613
- package/ios/Sources/LiteRTLM/Benchmark.swift +83 -0
- package/ios/Sources/LiteRTLM/Capabilities.swift +41 -0
- package/ios/Sources/LiteRTLM/Config.swift +172 -0
- package/ios/Sources/LiteRTLM/Conversation.swift +472 -0
- package/ios/Sources/LiteRTLM/Engine.swift +208 -0
- package/ios/Sources/LiteRTLM/ExperimentalFlags.swift +142 -0
- package/ios/Sources/LiteRTLM/LiteRTLMError.swift +156 -0
- package/ios/Sources/LiteRTLM/Message.swift +225 -0
- package/ios/Sources/LiteRTLM/Tool.swift +291 -0
- package/ios/Sources/LiteRTLM/ToolManager.swift +152 -0
- package/package.json +6 -5
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import Foundation
|
|
15
|
+
import OSLog
|
|
16
|
+
|
|
17
|
+
/// A struct to manage flags for APIs that are not yet considered mature.
|
|
18
|
+
///
|
|
19
|
+
/// These flags guard experimental APIs that may still undergo significant changes.
|
|
20
|
+
/// To use any experimental flags, first call `ExperimentalFlags.optIntoExperimentalAPIs()`.
|
|
21
|
+
public struct ExperimentalFlags {
|
|
22
|
+
private static var optedIn = false
|
|
23
|
+
|
|
24
|
+
private static let logger = Logger(
|
|
25
|
+
subsystem: "com.google.odml.litertlm.swift",
|
|
26
|
+
category: "ExperimentalFlags"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
public static func optIntoExperimentalAPIs() {
|
|
30
|
+
guard !optedIn else { return }
|
|
31
|
+
logger.info("EXPERIMENTAL: LiteRTLM: Opting into experimental APIs....")
|
|
32
|
+
optedIn = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static var _enableBenchmark: Bool = false
|
|
36
|
+
|
|
37
|
+
public static var enableBenchmark: Bool {
|
|
38
|
+
get { return _enableBenchmark }
|
|
39
|
+
set {
|
|
40
|
+
guard optedIn else {
|
|
41
|
+
logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
_enableBenchmark = newValue
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static var _convertCamelToSnakeCaseInToolDescription: Bool = true
|
|
49
|
+
|
|
50
|
+
/// Whether to convert the function and parameter names in to snake case for
|
|
51
|
+
/// tool calling.
|
|
52
|
+
///
|
|
53
|
+
/// Swift idiomatic style uses camelCase for names. However, many large
|
|
54
|
+
/// language models are predominantly trained on datasets where snake_case is
|
|
55
|
+
/// common.
|
|
56
|
+
///
|
|
57
|
+
/// By default, this API converts Swift camelCase names to snake_case to
|
|
58
|
+
/// potentially improve tool calling performance with models more familiar
|
|
59
|
+
/// with snake_case.
|
|
60
|
+
///
|
|
61
|
+
/// Set this flag to `false` if your model is specifically trained with
|
|
62
|
+
/// camelCase tool descriptions to skip the conversion. Otherwise, the default
|
|
63
|
+
/// of `true` (using snake_case) is recommended.
|
|
64
|
+
///
|
|
65
|
+
/// Note: This flag is read only when a new [Conversation] is created.
|
|
66
|
+
/// Changing this value will not affect any existing [Conversation] instances.
|
|
67
|
+
public static var convertCamelToSnakeCaseInToolDescription: Bool {
|
|
68
|
+
get { return _convertCamelToSnakeCaseInToolDescription }
|
|
69
|
+
set {
|
|
70
|
+
guard optedIn else {
|
|
71
|
+
logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
_convertCamelToSnakeCaseInToolDescription = newValue
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static var _enableConversationConstrainedDecoding: Bool = false
|
|
79
|
+
|
|
80
|
+
/// Whether to enable conversation constrained decoding. This is primarily
|
|
81
|
+
/// used for function calling.
|
|
82
|
+
///
|
|
83
|
+
/// Note: This flag is read only when a new [Conversation] is created.
|
|
84
|
+
/// Changing this value will not affect any existing [Conversation] instances.
|
|
85
|
+
public static var enableConversationConstrainedDecoding: Bool {
|
|
86
|
+
get { return _enableConversationConstrainedDecoding }
|
|
87
|
+
set {
|
|
88
|
+
guard optedIn else {
|
|
89
|
+
logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
_enableConversationConstrainedDecoding = newValue
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static var _enableSpeculativeDecoding: Bool? = nil
|
|
97
|
+
|
|
98
|
+
/// Whether to enable speculative decoding.
|
|
99
|
+
///
|
|
100
|
+
/// If true, enable speculative decoding; an error will be thrown if the model does not support it.
|
|
101
|
+
/// If false, disable speculative decoding.
|
|
102
|
+
///
|
|
103
|
+
/// Note: This flag is read only when a new [Engine] is created. Changing this value will not
|
|
104
|
+
/// affect any existing [Engine] or [Conversation] instances.
|
|
105
|
+
public static var enableSpeculativeDecoding: Bool? {
|
|
106
|
+
get { return _enableSpeculativeDecoding }
|
|
107
|
+
set {
|
|
108
|
+
guard optedIn else {
|
|
109
|
+
logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
_enableSpeculativeDecoding = newValue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static var _visualTokenBudget: Int32? = nil
|
|
117
|
+
|
|
118
|
+
/// The visual token budget.
|
|
119
|
+
///
|
|
120
|
+
/// The number of visual tokens that the model can generate for a single image. If null, there is
|
|
121
|
+
/// no budget limit and the engine use as much as needed.
|
|
122
|
+
///
|
|
123
|
+
/// Currently, this is only supported by Gemma4. If this flag is set for a non-Gemma4 model, it
|
|
124
|
+
/// will result in a no-ops. The Gemma4 budget options are 70, 140, 280, 560, or 1120 tokens. See
|
|
125
|
+
/// https://ai.google.dev/gemma/docs/capabilities/vision#variable-resolution for more details.
|
|
126
|
+
///
|
|
127
|
+
/// Note: This flag takes effect immediately and change alter the behaivor of created
|
|
128
|
+
/// [Conversation].
|
|
129
|
+
public static var visualTokenBudget: Int32? {
|
|
130
|
+
get { return _visualTokenBudget }
|
|
131
|
+
set {
|
|
132
|
+
guard optedIn else {
|
|
133
|
+
logger.error("LiteRTLM: Must opt into experimental APIs before setting this flag.")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
_visualTokenBudget = newValue
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prevent initializing the struct
|
|
141
|
+
private init() {}
|
|
142
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
/// Errors thrown by the LiteRT-LM Swift API.
|
|
18
|
+
public enum LiteRTLMError: Error, LocalizedError, Equatable {
|
|
19
|
+
case engine(EngineError)
|
|
20
|
+
case conversation(ConversationError)
|
|
21
|
+
case config(ConfigError)
|
|
22
|
+
case tool(ToolError)
|
|
23
|
+
case message(MessageError)
|
|
24
|
+
|
|
25
|
+
public var errorDescription: String? {
|
|
26
|
+
switch self {
|
|
27
|
+
case .engine(let error): return error.errorDescription
|
|
28
|
+
case .conversation(let error): return error.errorDescription
|
|
29
|
+
case .config(let error): return error.errorDescription
|
|
30
|
+
case .tool(let error): return error.errorDescription
|
|
31
|
+
case .message(let error): return error.errorDescription
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Specific errors related to the `Engine`.
|
|
36
|
+
public enum EngineError: Error, LocalizedError, Equatable {
|
|
37
|
+
case alreadyInitialized
|
|
38
|
+
case failedToCreateSettings
|
|
39
|
+
case failedToCreateEngine
|
|
40
|
+
case notInitialized
|
|
41
|
+
case failedToCreateSessionConfig
|
|
42
|
+
case failedToCreateConversationConfig
|
|
43
|
+
case failedToCreateConversation
|
|
44
|
+
|
|
45
|
+
public var errorDescription: String? {
|
|
46
|
+
switch self {
|
|
47
|
+
case .alreadyInitialized:
|
|
48
|
+
return "Engine is already initialized."
|
|
49
|
+
case .failedToCreateSettings:
|
|
50
|
+
return "Failed to create engine settings."
|
|
51
|
+
case .failedToCreateEngine:
|
|
52
|
+
return "Failed to create engine."
|
|
53
|
+
case .notInitialized:
|
|
54
|
+
return "Engine is not initialized."
|
|
55
|
+
case .failedToCreateSessionConfig:
|
|
56
|
+
return "Failed to create session config."
|
|
57
|
+
case .failedToCreateConversationConfig:
|
|
58
|
+
return "Failed to create conversation config."
|
|
59
|
+
case .failedToCreateConversation:
|
|
60
|
+
return "Failed to create conversation."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Specific errors related to `Conversation`.
|
|
66
|
+
public enum ConversationError: Error, LocalizedError, Equatable {
|
|
67
|
+
case notAlive
|
|
68
|
+
case failedToSerializeMessage
|
|
69
|
+
case invalidResponse(String)
|
|
70
|
+
case recurringToolCallLimitExceeded(limit: Int)
|
|
71
|
+
case failedToStartStream(status: Int)
|
|
72
|
+
case invalidJson(String)
|
|
73
|
+
case toolExecutionError(name: String, error: String)
|
|
74
|
+
case benchmarkNotEnabled
|
|
75
|
+
case benchmarkInfoUnavailable
|
|
76
|
+
|
|
77
|
+
public var errorDescription: String? {
|
|
78
|
+
switch self {
|
|
79
|
+
case .notAlive:
|
|
80
|
+
return "Conversation is not alive."
|
|
81
|
+
case .failedToSerializeMessage:
|
|
82
|
+
return "Failed to serialize message to JSON string."
|
|
83
|
+
case .invalidResponse(let details):
|
|
84
|
+
return "Invalid response from native layer: \(details)"
|
|
85
|
+
case .recurringToolCallLimitExceeded(let limit):
|
|
86
|
+
return "Exceeded recurring tool call limit of \(limit)"
|
|
87
|
+
case .failedToStartStream(let status):
|
|
88
|
+
return "Failed to start stream. Status: \(status)"
|
|
89
|
+
case .invalidJson(let details):
|
|
90
|
+
return "Invalid JSON: \(details)"
|
|
91
|
+
case .toolExecutionError(let name, let error):
|
|
92
|
+
return "Error processing tool call \(name): \(error)"
|
|
93
|
+
case .benchmarkNotEnabled:
|
|
94
|
+
return """
|
|
95
|
+
Benchmark flag is not enabled. Please enable the flag by setting setting \
|
|
96
|
+
ExperimentalFlags.enableBenchmark to true before initializing the Engine.
|
|
97
|
+
"""
|
|
98
|
+
case .benchmarkInfoUnavailable:
|
|
99
|
+
return "Failed to get benchmark info."
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public enum ConfigError: Error, LocalizedError, Equatable {
|
|
105
|
+
case invalidMaxNumTokens
|
|
106
|
+
case invalidMaxNumImages(count: Int)
|
|
107
|
+
case invalidTopK
|
|
108
|
+
case invalidTopP
|
|
109
|
+
case invalidTemperature
|
|
110
|
+
case multipleSystemMessages
|
|
111
|
+
|
|
112
|
+
public var errorDescription: String? {
|
|
113
|
+
switch self {
|
|
114
|
+
case .invalidMaxNumTokens:
|
|
115
|
+
return "maxNumTokens must be positive or nil (use the default from model or engine)."
|
|
116
|
+
case .invalidMaxNumImages:
|
|
117
|
+
return "maxNumImages must be non-negative or nil (use the default from model or engine)."
|
|
118
|
+
case .invalidTopK:
|
|
119
|
+
return "topK should be positive."
|
|
120
|
+
case .invalidTopP:
|
|
121
|
+
return "topP not between 0 and 1"
|
|
122
|
+
case .invalidTemperature:
|
|
123
|
+
return "temperature should be non-negative"
|
|
124
|
+
case .multipleSystemMessages:
|
|
125
|
+
return "Cannot set both systemMessage and have system messages in initialMessages."
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Specific errors related to tools.
|
|
131
|
+
public enum ToolError: Error, LocalizedError, Equatable {
|
|
132
|
+
case notFound(name: String)
|
|
133
|
+
|
|
134
|
+
public var errorDescription: String? {
|
|
135
|
+
switch self {
|
|
136
|
+
case .notFound(let name):
|
|
137
|
+
return "Tool '\(name)' not found."
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Specific errors related to messages.
|
|
143
|
+
public enum MessageError: Error, LocalizedError, Equatable {
|
|
144
|
+
case failedToConvertToJson
|
|
145
|
+
case invalidContent
|
|
146
|
+
|
|
147
|
+
public var errorDescription: String? {
|
|
148
|
+
switch self {
|
|
149
|
+
case .failedToConvertToJson:
|
|
150
|
+
return "Failed to convert Message to JSON string."
|
|
151
|
+
case .invalidContent:
|
|
152
|
+
return "No content found in JSON string. Cannot create Message."
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
import OSLog
|
|
17
|
+
|
|
18
|
+
/// The role of the message in a conversation.
|
|
19
|
+
public enum Role: String {
|
|
20
|
+
case system
|
|
21
|
+
case user
|
|
22
|
+
case model
|
|
23
|
+
case tool
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Represents a content in the `Message` of the conversation.
|
|
27
|
+
///
|
|
28
|
+
/// Example Usage:
|
|
29
|
+
/// ```swift
|
|
30
|
+
/// let textContent = Content.text("What's the capital of France?")
|
|
31
|
+
/// let imageContent = Content.imageData(imageData)
|
|
32
|
+
/// let audioContent = Content.audioFile(path: "/path/to/audio.wav")
|
|
33
|
+
/// ```
|
|
34
|
+
public enum Content {
|
|
35
|
+
/// Text.
|
|
36
|
+
case text(String)
|
|
37
|
+
/// Image provided as raw bytes.
|
|
38
|
+
case imageData(Data)
|
|
39
|
+
/// Image provided by an absolute file path.
|
|
40
|
+
case imageFile(String)
|
|
41
|
+
/// Audio provided as raw bytes.
|
|
42
|
+
case audioData(Data)
|
|
43
|
+
/// Audio provided by an absolute file path.
|
|
44
|
+
case audioFile(String)
|
|
45
|
+
|
|
46
|
+
/// Convert to JSON format. Used internally.
|
|
47
|
+
var toJson: [String: String] {
|
|
48
|
+
switch self {
|
|
49
|
+
case .text(let text):
|
|
50
|
+
return ["type": "text", "text": text]
|
|
51
|
+
case .imageData(let bytes):
|
|
52
|
+
return ["type": "image", "blob": bytes.base64EncodedString()]
|
|
53
|
+
case .imageFile(let absPath):
|
|
54
|
+
return ["type": "image", "path": absPath]
|
|
55
|
+
case .audioData(let bytes):
|
|
56
|
+
return ["type": "audio", "blob": bytes.base64EncodedString()]
|
|
57
|
+
case .audioFile(let absPath):
|
|
58
|
+
return ["type": "audio", "path": absPath]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Represents a message in the conversation. A message can contain multiple `Content`s.
|
|
64
|
+
///
|
|
65
|
+
/// Example Usage:
|
|
66
|
+
/// ```swift
|
|
67
|
+
/// let textMessage = Message("What's the capital of France?")
|
|
68
|
+
/// let modelMessage = Message("The capital of France is Paris.", role: .model)
|
|
69
|
+
/// let audioContentMessage = Message(of: someAudioContent)
|
|
70
|
+
/// let multiContentMessage = Message(of: someText, someImageContent)
|
|
71
|
+
/// let multiContentMessageFromArray = Message(contents: [someText, someImageContent])
|
|
72
|
+
/// ```
|
|
73
|
+
public struct Message {
|
|
74
|
+
|
|
75
|
+
private let logger = Logger(
|
|
76
|
+
subsystem: "com.google.odml.litertlm.swift",
|
|
77
|
+
category: "Message"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
/// The role of the message.
|
|
81
|
+
public let role: Role
|
|
82
|
+
|
|
83
|
+
/// The contents of the message.
|
|
84
|
+
public let contents: Contents
|
|
85
|
+
|
|
86
|
+
/// The channels of the message.
|
|
87
|
+
public let channels: [String: String]
|
|
88
|
+
|
|
89
|
+
// TODO: (b/459796564): Update constructor to `Message.role("messageTextHere")`
|
|
90
|
+
/// Creates a `Message` from a text string.
|
|
91
|
+
/// - Parameter text: The text content of the message.
|
|
92
|
+
public init(_ text: String, role: Role = .user, channels: [String: String] = [:]) {
|
|
93
|
+
self.init(contents: Contents(contents: [.text(text)]), role: role, channels: channels)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Creates a `Message` from one or more `Content`s.
|
|
97
|
+
/// - Parameter contents: The list of contents for this message.
|
|
98
|
+
public init(of contents: Content..., role: Role = .user) {
|
|
99
|
+
precondition(!contents.isEmpty, "Contents should not be empty.")
|
|
100
|
+
self.contents = Contents(contents: contents)
|
|
101
|
+
self.role = role
|
|
102
|
+
self.channels = [:]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Creates a `Message` from a list of `Content`s.
|
|
106
|
+
/// - Parameter contents: The list of contents for this message.
|
|
107
|
+
public init(contents: [Content], role: Role = .user, channels: [String: String] = [:]) {
|
|
108
|
+
precondition(
|
|
109
|
+
!contents.isEmpty || !channels.isEmpty, "Contents and channels should not both be empty.")
|
|
110
|
+
self.contents = Contents(contents: contents)
|
|
111
|
+
self.role = role
|
|
112
|
+
self.channels = channels
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Creates a `Message` from a `Contents` object.
|
|
116
|
+
/// - Parameter contents: The contents object for this message.
|
|
117
|
+
public init(contents: Contents, role: Role = .user, channels: [String: String] = [:]) {
|
|
118
|
+
precondition(
|
|
119
|
+
!contents.isEmpty || !channels.isEmpty, "Contents and channels should not both be empty.")
|
|
120
|
+
self.contents = contents
|
|
121
|
+
self.role = role
|
|
122
|
+
self.channels = channels
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Convert to JSON format. Used internally.
|
|
126
|
+
var toJson: [String: Any] {
|
|
127
|
+
var dict: [String: Any] = ["role": role.rawValue]
|
|
128
|
+
if !contents.isEmpty {
|
|
129
|
+
dict["content"] = contents.toJson
|
|
130
|
+
}
|
|
131
|
+
if !channels.isEmpty {
|
|
132
|
+
dict["channels"] = channels
|
|
133
|
+
}
|
|
134
|
+
return dict
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Convenience property to get the text content of the message.
|
|
138
|
+
/// Returns a string by concatenating all text content items, separated by a space.
|
|
139
|
+
/// Returns an empty string if no text content exists.
|
|
140
|
+
public var toString: String {
|
|
141
|
+
return contents.toString
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// A computed property that returns the JSON string representation
|
|
145
|
+
/// of the Message, or nil if conversion fails.
|
|
146
|
+
var jsonString: String {
|
|
147
|
+
get throws {
|
|
148
|
+
let jsonData = try JSONSerialization.data(withJSONObject: self.toJson, options: [])
|
|
149
|
+
guard let resultString = String(data: jsonData, encoding: .utf8) else {
|
|
150
|
+
throw LiteRTLMError.message(.failedToConvertToJson)
|
|
151
|
+
}
|
|
152
|
+
return resultString
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Represents a collection of `Content` items in the `Message` of the conversation.
|
|
158
|
+
///
|
|
159
|
+
/// Example Usage:
|
|
160
|
+
/// ```swift
|
|
161
|
+
/// let contents = Contents(contents: [.text("Hello")])
|
|
162
|
+
/// ```
|
|
163
|
+
public struct Contents: RandomAccessCollection {
|
|
164
|
+
public typealias Element = Content
|
|
165
|
+
public typealias Index = Int
|
|
166
|
+
|
|
167
|
+
/// The list of underlying content items.
|
|
168
|
+
public var contents: [Content]
|
|
169
|
+
|
|
170
|
+
public var startIndex: Int { contents.startIndex }
|
|
171
|
+
public var endIndex: Int { contents.endIndex }
|
|
172
|
+
|
|
173
|
+
public subscript(position: Int) -> Content {
|
|
174
|
+
return contents[position]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Creates a `Contents` from a list of `Content`s.
|
|
178
|
+
public init(contents: [Content]) {
|
|
179
|
+
self.contents = contents
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Creates an empty `Contents`.
|
|
183
|
+
public static func empty() -> Contents {
|
|
184
|
+
return Contents(contents: [])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Creates a `Contents` from a text string.
|
|
188
|
+
public static func of(_ text: String) -> Contents {
|
|
189
|
+
return Contents(contents: [.text(text)])
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Creates a `Contents` from one or more `Content`s.
|
|
193
|
+
public static func of(_ contents: Content...) -> Contents {
|
|
194
|
+
return Contents(contents: contents)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Creates a `Contents` from a list of `Content`s.
|
|
198
|
+
public static func of(_ contents: [Content]) -> Contents {
|
|
199
|
+
return Contents(contents: contents)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Convert to JSON format. Used internally.
|
|
203
|
+
var toJson: [[String: String]] {
|
|
204
|
+
return contents.map { $0.toJson }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// A computed property that returns the JSON string representation
|
|
208
|
+
/// of the Contents array.
|
|
209
|
+
var jsonString: String {
|
|
210
|
+
get throws {
|
|
211
|
+
let jsonData = try JSONSerialization.data(withJSONObject: self.toJson, options: [])
|
|
212
|
+
guard let resultString = String(data: jsonData, encoding: .utf8) else {
|
|
213
|
+
throw LiteRTLMError.message(.failedToConvertToJson)
|
|
214
|
+
}
|
|
215
|
+
return resultString
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Returns a string by concatenating all text content items, separated by a space.
|
|
220
|
+
public var toString: String {
|
|
221
|
+
contents.compactMap { content in
|
|
222
|
+
if case .text(let str) = content { return str } else { return nil }
|
|
223
|
+
}.joined(separator: " ")
|
|
224
|
+
}
|
|
225
|
+
}
|