LoginSignup
2
4

More than 1 year has passed since last update.

【iOS】OpenAI APIを使ってアプリを作る

Last updated at Posted at 2023-03-25

はじめに

流行りの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はこちらです
(ちょっと余計な実装も含めてますが、ざっくりこんな感じでいきます)

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]
で値を確認したところ

OpenAISwift.swift
        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を理解せずに動いてしまったところもあるので、ここから引き続きデモアプリを通して学んでいこうと思います

参考

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4