Skip to content

Commit 975b4f1

Browse files
feat: adds PDF support (#11)
1 parent bc58fc8 commit 975b4f1

File tree

7 files changed

+221
-20
lines changed

7 files changed

+221
-20
lines changed

Playground/Playground/ViewModels/AppViewModel.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,25 @@ final class AppViewModel {
2323
var temperature = 0.5
2424

2525
init() {
26-
if let existingApiKey = UserDefaults.standard.string(forKey: "apiKey") {
27-
self.apiKey = existingApiKey
28-
}
29-
30-
fetchModels()
3126
configureChat()
27+
fetchModels()
28+
}
29+
30+
func setHeaders(_ headers: [String: String]) {
31+
chat = LLMChatAnthropic(apiKey: apiKey, headers: headers)
3232
}
3333

3434
func saveSettings() {
3535
UserDefaults.standard.set(apiKey, forKey: "apiKey")
36-
37-
if let newApiKey = UserDefaults.standard.string(forKey: "apiKey") {
38-
self.apiKey = newApiKey
39-
}
40-
4136
configureChat()
4237
}
4338

4439
private func configureChat() {
45-
chat = LLMChatAnthropic(apiKey: apiKey, headers: ["anthropic-beta": "prompt-caching-2024-07-31"])
40+
if let apiKey = UserDefaults.standard.string(forKey: "apiKey") {
41+
self.apiKey = apiKey
42+
}
43+
44+
chat = LLMChatAnthropic(apiKey: apiKey)
4645
}
4746

4847
private func fetchModels() {

Playground/Playground/Views/AppView.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ struct AppView: View {
2626
NavigationLink("Tool Use") {
2727
ToolUseView()
2828
}
29-
29+
}
30+
31+
Section("Beta") {
3032
NavigationLink("Prompt Caching") {
3133
PromptCachingView()
3234
}
35+
36+
NavigationLink("PDF Support") {
37+
PDFSupportView()
38+
}
3339
}
3440
}
3541
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//
2+
// PDFSupportView.swift
3+
// Playground
4+
//
5+
// Created by Kevin Hermawan on 11/3/24.
6+
//
7+
8+
import SwiftUI
9+
import LLMChatAnthropic
10+
11+
struct PDFSupportView: View {
12+
@Environment(AppViewModel.self) private var viewModel
13+
@State private var isPreferencesPresented: Bool = false
14+
15+
@State private var document: String = "https://arxiv.org/pdf/1706.03762"
16+
@State private var prompt: String = "Explain this document"
17+
18+
@State private var response: String = ""
19+
@State private var inputTokens: Int = 0
20+
@State private var outputTokens: Int = 0
21+
@State private var totalTokens: Int = 0
22+
23+
var body: some View {
24+
@Bindable var viewModelBindable = viewModel
25+
26+
VStack {
27+
Form {
28+
Section("Prompts") {
29+
TextField("Document", text: $document)
30+
TextField("Prompt", text: $prompt)
31+
}
32+
33+
Section("Response") {
34+
Text(response)
35+
}
36+
37+
UsageSection(inputTokens: inputTokens, outputTokens: outputTokens, totalTokens: totalTokens)
38+
}
39+
40+
VStack {
41+
SendButton(stream: viewModel.stream, onSend: onSend, onStream: onStream)
42+
}
43+
}
44+
.onAppear {
45+
viewModel.setHeaders(["anthropic-beta": "pdfs-2024-09-25"])
46+
}
47+
.toolbar {
48+
ToolbarItem(placement: .principal) {
49+
NavigationTitle("PDF Support")
50+
}
51+
52+
ToolbarItem(placement: .primaryAction) {
53+
Button("Preferences", systemImage: "gearshape", action: { isPreferencesPresented.toggle() })
54+
}
55+
}
56+
.sheet(isPresented: $isPreferencesPresented) {
57+
PreferencesView()
58+
}
59+
}
60+
61+
private func onSend() {
62+
clear()
63+
64+
let messages = [
65+
ChatMessage(role: .system, content: viewModel.systemPrompt),
66+
ChatMessage(role: .user, content: [.text(prompt), .document(document)])
67+
]
68+
69+
let options = ChatOptions(temperature: viewModel.temperature)
70+
71+
Task {
72+
do {
73+
let completion = try await viewModel.chat.send(model: viewModel.selectedModel, messages: messages, options: options)
74+
75+
if let text = completion.content.first?.text {
76+
self.response = text
77+
}
78+
79+
if let usage = completion.usage {
80+
self.inputTokens = usage.inputTokens
81+
self.outputTokens = usage.outputTokens
82+
self.totalTokens = usage.totalTokens
83+
}
84+
} catch {
85+
print(String(describing: error))
86+
}
87+
}
88+
}
89+
90+
private func onStream() {
91+
clear()
92+
93+
let messages = [
94+
ChatMessage(role: .system, content: viewModel.systemPrompt),
95+
ChatMessage(role: .user, content: [.text(prompt), .document(document)])
96+
]
97+
98+
let options = ChatOptions(temperature: viewModel.temperature)
99+
100+
Task {
101+
do {
102+
for try await chunk in viewModel.chat.stream(model: viewModel.selectedModel, messages: messages, options: options) {
103+
if let text = chunk.delta?.text {
104+
self.response += text
105+
}
106+
107+
if let usage = chunk.usage {
108+
self.inputTokens = usage.inputTokens
109+
self.outputTokens = usage.outputTokens
110+
self.totalTokens = usage.totalTokens
111+
}
112+
}
113+
} catch {
114+
print(String(describing: error))
115+
}
116+
}
117+
}
118+
119+
private func clear() {
120+
self.response = ""
121+
self.inputTokens = 0
122+
self.outputTokens = 0
123+
self.totalTokens = 0
124+
}
125+
}

Playground/Playground/Views/PromptCachingView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ struct PromptCachingView: View {
5050
SendButton(stream: viewModel.stream, onSend: onSend, onStream: onStream)
5151
}
5252
}
53+
.onAppear {
54+
viewModel.setHeaders(["anthropic-beta": "prompt-caching-2024-07-31"])
55+
}
5356
.toolbar {
5457
ToolbarItem(placement: .principal) {
5558
NavigationTitle("Prompt Caching")

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,31 @@ let task = Task {
193193

194194
To learn more about prompt caching, check out the [Anthropic documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching).
195195

196+
#### PDF Support (Beta)
197+
198+
```swift
199+
let chat = LLMChatAnthropic(
200+
apiKey: "<YOUR_ANTHROPIC_API_KEY>",
201+
headers: ["anthropic-beta": "pdfs-2024-09-25"] // Required
202+
)
203+
204+
let messages = [
205+
ChatMessage(role: .user, content: [.text("Explain this document"), .document(document)])
206+
]
207+
208+
let task = Task {
209+
do {
210+
let completion = try await chat.send(model: "claude-3-5-sonnet", messages: messages)
211+
212+
print(completion.content.first?.text ?? "No response")
213+
} catch {
214+
print(String(describing: error))
215+
}
216+
}
217+
```
218+
219+
To learn more about PDF support, check out the [Anthropic documentation](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support).
220+
196221
### Error Handling
197222

198223
`LLMChatAnthropic` provides structured error handling through the `LLMChatAnthropicError` enum. This enum contains three cases that represent different types of errors you might encounter:

Sources/LLMChatAnthropic/ChatMessage.swift

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ public struct ChatMessage: Encodable, Sendable {
1212
/// The role of the participant in the chat conversation.
1313
public let role: Role
1414

15-
/// The content of the message, which can be text or image.
15+
/// The content of the message, which can be text, image, or document.
1616
public let content: [Content]
1717

18-
/// The cache control settings for the message. Only applicable when the role is `system`.
18+
/// The cache control settings for the message.
1919
public var cacheControl: CacheControl?
2020

2121
/// An enum that represents the role of a participant in the chat.
@@ -33,6 +33,9 @@ public struct ChatMessage: Encodable, Sendable {
3333
/// A case that represents image content.
3434
case image(String)
3535

36+
/// A case that represents document content.
37+
case document(String)
38+
3639
public func encode(to encoder: Encoder) throws {
3740
var container = encoder.container(keyedBy: CodingKeys.self)
3841

@@ -45,7 +48,7 @@ public struct ChatMessage: Encodable, Sendable {
4548
var sourceContainer = container.nestedContainer(keyedBy: SourceCodingKeys.self, forKey: .source)
4649

4750
if imageString.hasPrefix("http://") || imageString.hasPrefix("https://") {
48-
let (base64String, mediaType) = Content.convertImageUrlToBase64(url: imageString)
51+
let (base64String, mediaType) = Content.convertFileToBase64(url: imageString)
4952
try sourceContainer.encode("base64", forKey: .type)
5053
try sourceContainer.encode(mediaType, forKey: .mediaType)
5154
try sourceContainer.encode(base64String, forKey: .data)
@@ -55,6 +58,20 @@ public struct ChatMessage: Encodable, Sendable {
5558
try sourceContainer.encode(mediaType, forKey: .mediaType)
5659
try sourceContainer.encode(imageString, forKey: .data)
5760
}
61+
case .document(let documentString):
62+
try container.encode("document", forKey: .type)
63+
var sourceContainer = container.nestedContainer(keyedBy: SourceCodingKeys.self, forKey: .source)
64+
65+
if documentString.hasPrefix("http://") || documentString.hasPrefix("https://") {
66+
let (base64String, mediaType) = Content.convertFileToBase64(url: documentString)
67+
try sourceContainer.encode("base64", forKey: .type)
68+
try sourceContainer.encode(mediaType, forKey: .mediaType)
69+
try sourceContainer.encode(base64String, forKey: .data)
70+
} else {
71+
try sourceContainer.encode("base64", forKey: .type)
72+
try sourceContainer.encode("application/pdf", forKey: .mediaType)
73+
try sourceContainer.encode(documentString, forKey: .data)
74+
}
5875
}
5976
}
6077

@@ -66,13 +83,13 @@ public struct ChatMessage: Encodable, Sendable {
6683
case type, mediaType = "media_type", data
6784
}
6885

69-
private static func convertImageUrlToBase64(url: String) -> (String, String) {
70-
guard let imageUrl = URL(string: url), let imageData = try? Data(contentsOf: imageUrl) else {
86+
private static func convertFileToBase64(url: String) -> (String, String) {
87+
guard let fileUrl = URL(string: url), let fileData = try? Data(contentsOf: fileUrl) else {
7188
return ("", "")
7289
}
7390

74-
let base64String = imageData.base64EncodedString()
75-
let mediaType = detectMediaType(from: imageData)
91+
let base64String = fileData.base64EncodedString()
92+
let mediaType = detectMediaType(from: fileData)
7693

7794
return (base64String, mediaType)
7895
}
@@ -96,6 +113,8 @@ public struct ChatMessage: Encodable, Sendable {
96113
return "image/gif"
97114
} else if bytes.starts(with: [0x52, 0x49, 0x46, 0x46]) && String(data: data.subdata(in: 8..<12), encoding: .ascii) == "WEBP" {
98115
return "image/webp"
116+
} else if bytes.starts(with: [0x25, 0x50, 0x44, 0x46]) {
117+
return "application/pdf"
99118
} else {
100119
return ""
101120
}
@@ -120,7 +139,6 @@ public struct ChatMessage: Encodable, Sendable {
120139
}
121140
}
122141

123-
124142
/// Creates a new instance of ``ChatMessage``.
125143
/// - Parameters:
126144
/// - role: The role of the participant.

Sources/LLMChatAnthropic/Documentation.docc/Documentation.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,31 @@ let task = Task {
164164

165165
To learn more about prompt caching, check out the [Anthropic documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching).
166166

167+
#### PDF Support (Beta)
168+
169+
```swift
170+
let chat = LLMChatAnthropic(
171+
apiKey: "<YOUR_ANTHROPIC_API_KEY>",
172+
headers: ["anthropic-beta": "pdfs-2024-09-25"] // Required
173+
)
174+
175+
let messages = [
176+
ChatMessage(role: .user, content: [.text("Explain this document"), .document(document)])
177+
]
178+
179+
let task = Task {
180+
do {
181+
let completion = try await chat.send(model: "claude-3-5-sonnet", messages: messages)
182+
183+
print(completion.content.first?.text ?? "No response")
184+
} catch {
185+
print(String(describing: error))
186+
}
187+
}
188+
```
189+
190+
To learn more about PDF support, check out the [Anthropic documentation](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support).
191+
167192
### Error Handling
168193

169194
``LLMChatAnthropic`` provides structured error handling through the ``LLMChatAnthropicError`` enum. This enum contains three cases that represent different types of errors you might encounter:

0 commit comments

Comments
 (0)