Skip to content

Commit 864924a

Browse files
authored
Merge pull request #4 from SwiftedMind/develop
2.0.0 Release Merge
2 parents 7413619 + 7e68bd5 commit 864924a

11 files changed

+365
-68
lines changed

Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import PackageDescription
55

66
let package = Package(
77
name: "GPTSwift",
8-
platforms: [.iOS(.v13), .macOS(.v10_15)],
8+
platforms: [
9+
.iOS(.v13),
10+
.macOS(.v10_15),
11+
.watchOS(.v8),
12+
.tvOS(.v15)
13+
],
914
products: [
1015
.library(
1116
name: "GPTSwift",

README.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1-
![GPTSwiftBanner](https://user-images.githubusercontent.com/7083109/222953457-ffaa5920-64c2-4b04-b6ad-e1341b89732c.png)
2-
1+
<picture>
2+
<source width="150px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/7083109/224795534-47eaf18c-2032-48a9-a453-3dba6fbd7699.png">
3+
<img width="150px" alt="GPTSwift Logo. Light: 'Light Mode' Dark: 'Dark Mode'" src="https://user-images.githubusercontent.com/7083109/224795540-5a1938ed-b829-40d3-aa67-3a51fa5de904.png">
4+
</picture>
35

46
# GPTSwift
57
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSwiftedMind%2FGPTSwift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/SwiftedMind/GPTSwift)
68
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSwiftedMind%2FGPTSwift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/SwiftedMind/GPTSwift)
79

810

11+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSwiftedMind%2FGPTSwift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/SwiftedMind/GPTSwift)
12+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSwiftedMind%2FGPTSwift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/SwiftedMind/GPTSwift)
13+
![GitHub](https://img.shields.io/github/license/SwiftedMind/GPTSwift)
14+
915
GPTSwift is a lightweight and convenient wrapper around the OpenAI API. It is completely written in Swift. Currently, only ChatGPT's API is supported.
1016

1117
Using GPTSwift is easy:
1218

1319
```swift
14-
let gptSwift = GPTSwift(apiKey: "YOUR_API_KEY")
15-
try await gptSwift.askChatGPT("What is the answer to life, the universe and everything in it?")
20+
let gptSwift = ChatGPTSwift(apiKey: "YOUR_API_KEY")
21+
try await gptSwift.ask("What is the answer to life, the universe and everything in it?")
22+
23+
// Stream the answers as they are generated
24+
var answer = ""
25+
for try await nextWord in try await gptSwift.streamedAnswer.ask("Tell me a story about birds") {
26+
answer += nextWord
27+
}
1628
```
1729

1830
An example project can be found here: [GPTPlayground](https://github.com/SwiftedMind/GPTPlayground/tree/main)
@@ -29,17 +41,14 @@ An example project can be found here: [GPTPlayground](https://github.com/Swifted
2941

3042
### Requirements
3143

32-
- iOS 13+
33-
- macOS 10.15+
34-
35-
You will also need Swift 5.7 to compile the package.
44+
GPTSwift supports iOS 15+, macOS 12+, watchOS 8+ and tvOS 15+.
3645

3746
### Installation
3847

3948
The package is installed through the Swift Package Manager. Simply add the following line to your `Package.swift` dependencies:
4049

4150
```swift
42-
.package(url: "https://github.com/SwiftedMind/GPTSwift", from: "1.0.0")
51+
.package(url: "https://github.com/SwiftedMind/GPTSwift", from: "2.0.0")
4352
```
4453

4554
Alternatively, if you want to add the package to an Xcode project, go to `File` > `Add Packages...` and enter the URL "https://github.com/SwiftedMind/GPTSwift" into the search field at the top. GPTSwift should appear in the list. Select it and click "Add Package" in the bottom right.
@@ -55,14 +64,14 @@ GPTSwift is just a lightweight wrapper around the API. Here are a few examples:
5564
import GPTSwift
5665

5766
func askChatGPT() async throws {
58-
let gptSwift = GPTSwift(apiKey: "YOUR_API_KEY")
67+
let gptSwift = ChatGPTSwift(apiKey: "YOUR_API_KEY")
5968

6069
// Basic query
61-
let firstResponse = try await gptSwift.askChatGPT("What is the answer to life, the universe and everything in it?")
70+
let firstResponse = try await gptSwift.ask("What is the answer to life, the universe and everything in it?")
6271
print(firstResponse.choices.map(\.message))
6372

6473
// Send multiple messages
65-
let secondResponse = try await gptSwift.askChatGPT(
74+
let secondResponse = try await gptSwift.ask(
6675
messages: [
6776
ChatMessage(role: .system, content: "You are a dog."),
6877
ChatMessage(role: .user, content: "Do you actually like playing fetch?")
@@ -80,11 +89,32 @@ func askChatGPT() async throws {
8089
fullRequest.temperature = 0.8
8190
fullRequest.numberOfAnswers = 2
8291

83-
let thirdResponse = try await gptSwift.askChatGPT(request: fullRequest)
92+
let thirdResponse = try await gptSwift.ask(with: fullRequest)
8493
print(thirdResponse.choices.map(\.message))
8594
}
8695
```
8796

97+
### Streaming answers
98+
99+
All of the above methods have a variant that lets you stream GPT's answer word for word, right as they are generated. The stream is provided to you via an `AsyncThrowingStream`. All you have to do is add a `streamedAnswer` before the call to `ask()`. For example:
100+
101+
```swift
102+
import GPTSwift
103+
104+
// In your view model
105+
@Published var gptAnswer = ""
106+
107+
func askChatGPT() async throws {
108+
let gptSwift = ChatGPTSwift(apiKey: "YOUR_API_KEY")
109+
110+
// Basic query
111+
gptAnswer = ""
112+
for try await nextWord in try await gptSwift.streamedAnswer.ask("Tell me a story about birds") {
113+
gptAnswer += nextWord
114+
}
115+
}
116+
```
117+
88118
For more information about the API, you can look at OpenAI's documentation:
89119
- [ChatGPT API Introduction](https://platform.openai.com/docs/guides/chat/chat-completions-beta)
90120
- [ChatGPT API documentation](https://platform.openai.com/docs/api-reference/chat/create)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// Copyright © 2023 Dennis Müller and all collaborators
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in all
12+
// copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
// SOFTWARE.
21+
//
22+
23+
import Foundation
24+
import Get
25+
26+
extension ChatGPTSwift {
27+
public class StreamedAnswer {
28+
private let client: APIClient
29+
private let apiKey: String
30+
31+
init(client: APIClient, apiKey: String) {
32+
self.client = client
33+
self.apiKey = apiKey
34+
}
35+
36+
/// Ask ChatGPT a single prompt without any special configuration.
37+
/// - Parameter userPrompt: The prompt to send
38+
/// - Returns: The response.
39+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
40+
public func ask(_ userPrompt: String) async throws -> AsyncThrowingStream<String, Swift.Error> {
41+
let chatRequest = ChatRequest(messages: [.init(role: .user, content: userPrompt)], stream: true)
42+
return try await ask(with: chatRequest)
43+
}
44+
45+
/// Ask ChatGPT something by sending multiple messages without any special configuration.
46+
/// - Parameter messages: The chat messages.
47+
/// - Returns: The response.
48+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
49+
public func ask(messages: [ChatMessage]) async throws -> AsyncThrowingStream<String, Swift.Error> {
50+
let chatRequest = ChatRequest(messages: messages, stream: true)
51+
return try await ask(with: chatRequest)
52+
}
53+
54+
/// Ask ChatGPT something by providing a chat request object, giving you full control over the request's configuration.
55+
/// - Parameter request: The request.
56+
/// - Returns: The response.
57+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
58+
public func ask(with chatRequest: ChatRequest) async throws -> AsyncThrowingStream<String, Swift.Error> {
59+
let request = Request(path: API.v1ChatCompletion, method: .post, body: chatRequest)
60+
var urlRequest = try await client.makeURLRequest(for: request)
61+
addHeaders(to: &urlRequest)
62+
63+
let (result, response) = try await client.session.bytes(for: urlRequest)
64+
65+
guard let response = response as? HTTPURLResponse else {
66+
throw Error.invalidResponse
67+
}
68+
69+
guard (200...299).contains(response.statusCode) else {
70+
throw Error.unacceptableStatusCode(code: response.statusCode, message: "")
71+
}
72+
73+
return AsyncThrowingStream { continuation in
74+
Task {
75+
do {
76+
for try await line in result.lines {
77+
if let chatResponse = line.asStreamedResponse {
78+
79+
// Ignore lines where only the role is specified
80+
if chatResponse.choices.first?.delta.role != nil {
81+
continue
82+
}
83+
84+
if let message = chatResponse.choices.first?.delta.content {
85+
continuation.yield(message)
86+
} else {
87+
break
88+
}
89+
} else {
90+
break
91+
}
92+
}
93+
} catch {
94+
throw Error.networkError(error)
95+
}
96+
97+
continuation.finish()
98+
}
99+
}
100+
}
101+
102+
private func addHeaders(to request: inout URLRequest) {
103+
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
104+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
105+
}
106+
}
107+
}
108+
109+
extension ChatGPTSwift.StreamedAnswer {
110+
public enum Error: Swift.Error {
111+
case invalidResponse
112+
case unacceptableStatusCode(code: Int, message: String)
113+
case networkError(Swift.Error)
114+
}
115+
}
116+
117+
private let decoder = JSONDecoder()
118+
private extension String {
119+
var asStreamedResponse: ChatStreamedResponse? {
120+
guard hasPrefix("data: "),
121+
let data = dropFirst(6).data(using: .utf8) else {
122+
return nil
123+
}
124+
return try! decoder.decode(ChatStreamedResponse.self, from: data)
125+
}
126+
}

Sources/GPTSwift/GPTSwift.swift renamed to Sources/GPTSwift/ChatGPT/ChatGPTSwift.swift

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@
2323
import Foundation
2424
import Get
2525

26-
/// A simple wrapper around the API for OpenAI.
27-
///
28-
/// Currently only supporting ChatGPT's model.
29-
public class GPTSwift: APIClientDelegate {
26+
/// A simple wrapper around the API for OpenAI's ChatGPT.
27+
public class ChatGPTSwift: APIClientDelegate {
3028

3129
private let client: APIClient
3230
private let apiClientRequestHandler: APIClientRequestHandler
3331

32+
/// A version of GPTSwift that streams all the answers.
33+
public let streamedAnswer: StreamedAnswer
34+
3435
/// A simple wrapper around the API for OpenAI.
3536
///
3637
/// Currently only supporting ChatGPT's model.
@@ -40,12 +41,13 @@ public class GPTSwift: APIClientDelegate {
4041
self.client = APIClient(baseURL: URL(string: API.base)) { [apiClientRequestHandler] configuration in
4142
configuration.delegate = apiClientRequestHandler
4243
}
44+
self.streamedAnswer = .init(client: client, apiKey: apiKey)
4345
}
4446

4547
/// Ask ChatGPT a single prompt without any special configuration.
4648
/// - Parameter userPrompt: The prompt to send
4749
/// - Returns: The response.
48-
public func askChatGPT(_ userPrompt: String) async throws -> ChatResponse {
50+
public func ask(_ userPrompt: String) async throws -> ChatResponse {
4951
let request = Request<ChatResponse>(
5052
path: API.v1ChatCompletion,
5153
method: .post,
@@ -54,29 +56,30 @@ public class GPTSwift: APIClientDelegate {
5456
])
5557
)
5658

59+
5760
return try await client.send(request).value
5861
}
5962

60-
/// Ask ChatGPT something by providing a chat request object, giving you full control over the request's configuration.
61-
/// - Parameter request: The request.
63+
/// Ask ChatGPT something by sending multiple messages without any special configuration.
64+
/// - Parameter messages: The chat messages.
6265
/// - Returns: The response.
63-
public func askChatGPT(request: ChatRequest) async throws -> ChatResponse {
66+
public func ask(messages: [ChatMessage]) async throws -> ChatResponse {
6467
let request = Request<ChatResponse>(
6568
path: API.v1ChatCompletion,
6669
method: .post,
67-
body: request
70+
body: ChatRequest(messages: messages)
6871
)
6972
return try await client.send(request).value
7073
}
7174

72-
/// Ask ChatGPT something by sending multiple messages without any special configuration.
73-
/// - Parameter messages: The chat messages.
75+
/// Ask ChatGPT something by providing a chat request object, giving you full control over the request's configuration.
76+
/// - Parameter request: The request.
7477
/// - Returns: The response.
75-
public func askChatGPT(messages: [ChatMessage]) async throws -> ChatResponse {
78+
public func ask(with request: ChatRequest) async throws -> ChatResponse {
7679
let request = Request<ChatResponse>(
7780
path: API.v1ChatCompletion,
7881
method: .post,
79-
body: ChatRequest(messages: messages)
82+
body: request
8083
)
8184
return try await client.send(request).value
8285
}

Sources/GPTSwift/Models/ChatMessage.swift renamed to Sources/GPTSwift/ChatGPT/Models/ChatMessage.swift

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,14 @@ import Foundation
3131
public struct ChatMessage: Codable {
3232

3333
/// The role of the message.
34-
public var role: Role
34+
public var role: ChatMessageRole
3535

3636
/// The content of the message.
3737
public var content: String
3838

3939
/// A message is a part of the chat conversation.
40-
public init(role: Role, content: String) {
40+
public init(role: ChatMessageRole, content: String) {
4141
self.role = role
4242
self.content = content
4343
}
4444
}
45-
46-
extension ChatMessage {
47-
48-
/// A role can be seen as the "owner", or "author" of a given message. You use this to allow GPT to differentiate between actual user prompts, behavior instructions (by you, the developer) and previous answers by GPT.
49-
///
50-
/// For more information, see the [OpenAI documentation](https://platform.openai.com/docs/guides/chat/introduction).
51-
public enum Role: String, Codable {
52-
53-
/// The user role is the used for the prompts made by the end-user.
54-
case user
55-
56-
/// The system role is used to instruct GPT on how to behave and what to generate.
57-
case system
58-
59-
/// The assistant role is usually meant to indicate that a message originates from a previous GPT answer.
60-
///
61-
/// This is useful because it allows GPT to recall previous answers and know about the general context of the conversation.
62-
case assistant
63-
}
64-
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Dennis Müller on 05.03.23.
6+
//
7+
8+
import Foundation
9+
10+
/// A role can be seen as the "owner", or "author" of a given message. You use this to allow GPT to differentiate between actual user prompts, behavior instructions (by you, the developer) and previous answers by GPT.
11+
///
12+
/// For more information, see the [OpenAI documentation](https://platform.openai.com/docs/guides/chat/introduction).
13+
public enum ChatMessageRole: String, Codable {
14+
15+
/// The user role is the used for the prompts made by the end-user.
16+
case user
17+
18+
/// The system role is used to instruct GPT on how to behave and what to generate.
19+
case system
20+
21+
/// The assistant role is usually meant to indicate that a message originates from a previous GPT answer.
22+
///
23+
/// This is useful because it allows GPT to recall previous answers and know about the general context of the conversation.
24+
case assistant
25+
}

0 commit comments

Comments
 (0)