はじめに
流行りのOpenAI APIを使ったアプリ開発を体験したくチュートリアル的な形でデモアプリを作ってみました。
環境
Xcode 14.1
内容
デモアプリはSwiftUIで作っていきます
Swift Package IndexでOpenAI APIが使えるライブラリを検索してこちらが1番良さそうでしたので使っていきます
新しいプロジェクトを作成したら、さっそく先ほどのライブラリを追加していきます
Pacakage Dependencies
の➕ボタンより
https://github.com/adamrushy/OpenAISwift.git
で検索
OpenAISwift
を追加します

これでPackage Dependenciesに追加されました
OpenAIのAPI keyを発行する
OpenAIのアカウントページのUserのAPI Keysより発行しておきます

ContentView
にOpen AIの処理を追加
OpenAIのAPIを使う処理だけで見れば
ざっくり以下3点追加することで実現できました
import OpenAISwift
OpenAISwift(authToken:)
openAI.sendCompletion(with:)
入力と回答を表示するUIの実装も含めて全体のContentViewはこちらです
(ちょっと余計な実装も含めてますが、ざっくりこんな感じでいきます)
import SwiftUI
+ import OpenAISwift
struct ContentView: View {
@FocusState var focus
@State var loading: Bool = false
@State var currentQuestion: String = ""
@State var responseList: [AIResponse] = []
@State var error: Error?
+ let openAI = OpenAISwift(authToken: "your_OpenAI_API_Key")
struct AIResponse: Identifiable {
let id: UUID = UUID()
let question: String
let answer: String
}
var body: some View {
ZStack {
VStack(spacing: 0) {
List {
ForEach(responseList) { response in
Section {
Text(response.answer)
.textSelection(.enabled)
} header: {
Text(response.question)
.onTapGesture {
currentQuestion = response.question
}
}
}
}
.listStyle(.grouped)
Divider()
HStack {
TextField("Ask me anything", text: $currentQuestion, axis: .vertical)
.lineLimit(2...10)
.border(Color(UIColor.separator))
.focused($focus)
Button {
askAI(question: currentQuestion)
} label: {
Image(systemName: "brain.head.profile")
}
.disabled(currentQuestion.isEmpty || loading)
}
.padding()
}
.onTapGesture {
focus = false
}
if loading {
ProgressView("asking...")
.progressViewStyle(.circular)
}
}
.alert(error?.localizedDescription ?? "エラーが発生しました", isPresented: .constant(error != nil), actions: {
Button("OK", role: .cancel) {
error = nil
}
})
}
func askAI(question: String) {
Task {
loading = true
do {
+ let result = try await openAI.sendCompletion(
+ with: question,
+ model: .gpt3(.davinci), // optional `OpenAIModelType`
+ maxTokens: 16, // optional `Int?`
+ temperature: 1 // optional `Double?`
+ )
loading = false
guard let answer = result.choices.first?.text else { throw NSError() }
let response = AIResponse(question: question, answer: answer)
responseList.insert(response, at: 0)
} catch {
loading = false
self.error = error
}
}
}
}

OpenAI<TextResult>(
object: "text_completion",
model: Optional("text-davinci-003"),
choices: [OpenAISwift.TextResult(text: "\n\n・Paella(パエリア)\n")]
)
openAI.sendCompletion(with:)
の部分はREADMEのまま追加してますが、これだと回答の表示がほとんどされません
openAI.sendCompletion(with:)
のコメントとOpenAI APIのドキュメントを見ると
/// Send a Completion to the OpenAI API
/// - Parameters:
/// - prompt: The Text Prompt
/// - model: The AI Model to Use. Set to `OpenAIModelType.gpt3(.davinci)` by default which is the most capable model
/// - maxTokens: The limit character for the returned response, defaults to 16 as per the API
/// - temperature: Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. Defaults to 1
/// - Returns: Returns an OpenAI Data Model
@available(swift 5.5)
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func sendCompletion(with prompt: String, model: OpenAIModelType = .gpt3(.davinci), maxTokens: Int = 16, temperature: Double = 1) async throws -> OpenAI<TextResult> {
return try await withCheckedThrowingContinuation { continuation in
sendCompletion(with: prompt, model: model, maxTokens: maxTokens, temperature: temperature) { result in
continuation.resume(with: result)
}
}
}
model: Optional("text-davinci-003")
でレスポンスが返ってきてたのでこれっぽい?
maxTokens
を変更することで表示できる文字数を増やせるようです
特にリリースする予定もないので最大の値を入れて動かしてみます
maxTokensの値は4073にしたらいけましたが、4074にしたらエラーになりました🤔
【2023/04/30追記】
このmaxTokensの考え方を勘違いしていたので訂正します🙏
使っているモデルのMax Tokensは、4,092なので、リクエストとレスポンスの合計が超えるとエラーになるようです。つまり、4074でエラーになったのはリクエストが残り分を占めていたので発生しただけでした。
このまま4073にしてしまうと、少し長い質問は受け付けてくれなくなりました。
ちなみにこの場合のエラーはよく分からなかったですが
decodingError(error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "object", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"object\", intValue: nil) (\"object\").", underlyingError: nil)))
レスポンスをdecodeしている箇所で止め
po JSONSerialization.jsonObject(with: success, options: []) as? [String: Any]
で値を確認したところ
makeRequest(request: request) { result in
switch result {
case .success(let success):
do {
let res = try JSONDecoder().decode(OpenAI<TextResult>.self, from: success)
completionHandler(.success(res))
} catch {
completionHandler(.failure(.decodingError(error: error)))
}
case .failure(let failure):
completionHandler(.failure(.genericError(error: failure)))
}
}
This model's maximum context length is 4097 tokens, however you requested 4162 tokens (72 in your prompt; 4090 for the completion). Please reduce your prompt; or completion length.
とエラー文言が確認できました

maxTokensの値を修正して、再度動作確認!
let result = try await openAI.sendCompletion(
with: question,
maxTokens: 2000
)

OpenAI<TextResult>(
object: "text_completion",
model: Optional("text-davinci-003"),
choices: [OpenAISwift.TextResult(
text: "\n\n- パエリア(Paella)\n- ジャモーサ(Gazpacho)\n- チキン・パパリラ(Pollo al ajillo)\n- タンベジータ・ラテ(Tortilla de patatas)\n- ケチャップ・ブイキャップ(Chorizo a la Sidra)\n- オリーヴ・オイル・ブレッド(Pan con Aceite)\n- ナキューロ(Nacionales)\n- クレア・レモン(Crema de limon)\n- ピーナッツ・ディップ(Crayonicas)\n- ビーフ・テツェッド(Carne mechada)")]
)
OpenAI<TextResult>(
object: "text_completion",
model: Optional("text-davinci-003"),
choices: [OpenAISwift.TextResult(
text: "\n\n・パエリア\n・チリリブ\n・パプリカ\n・トマトのサラダ\n・マルセロ・アルモド\n・パイナップルのタルタル\n・クーパンダース\n・パスタ・アルフレド\n・ジンファンデル\n・アルボ・ガーリャ")]
)
いい感じで動くようになりました👏
# トークンの消費量
最後に、今回ざっくり動くところまでOpenAi APIを触ってみましたが、どのくらいお金がかかったのか見てみます


$0.15でした
このあたりはまだまだ理解が足りてないですが、工夫次第で少ない消費にすることも出来そうな気がしました
おまけ(2023/04/30追記)
一問一答だけでなく、チャットもできるようにしてみました。

openAI.sendChat(with:)
にChatMessage
を渡すと前回の回答を踏まえた答えがもらえます
private func chatAI(question: String) {
Task {
loading = true
do {
var chat: [ChatMessage] = [ChatMessage(role: .system, content: "あなたはとても優秀なアシスタントで様々な質問に丁寧な言葉で正しく回答します")]
let chatHistory: [ChatMessage] = responseList
.map({ res in
[
ChatMessage(role: .assistant, content: res.answer),
ChatMessage(role: .user, content: res.question)
]
})
.flatMap { $0 }
.reversed()
chat.append(contentsOf: chatHistory)
chat.append(ChatMessage(role: .user, content: question))
let result = try await openAI.sendChat(
with: chat,
model: .chat(.chatgpt)
)
loading = false
guard let answer = result.choices.first?.message else {
throw NSError()
}
let response = AIResponse(question: question, answer: answer.content)
responseList.insert(response, at: 0)
} catch {
loading = false
self.error = error
}
}
}
ただしChatMessage
が空の状態(初回など)でリクエストをすると
[] is too short - 'messages'
とエラーが返ってきたので
初回はopenAI.sendCompletion
を使うようにしました。
まとめ
動くものは作れましたが、ほとんどOpenAI APIを理解せずに動いてしまったところもあるので、ここから引き続きデモアプリを通して学んでいこうと思います
参考