【Swift】swiftからChatGPT APIを使ってみる

Swift

ここ暫く話題になっているChatGPTですが、3月1日にAPIが公開されました。
元々OpenAI APIがあり、そこにChatGPTのAPIが追加された形になります。
今回はswiftからこのChatGPT APIを使ってみます。

前書き

今回はSwiftUIアプリでこのChatGPT APIを使ってみます。
なおSwiftUIを使う理由は私が慣れているからというだけで、
UIKitでもChatGPT APIに関わる部分は変わりません。

ChatGPT APIでは簡単なリクエストをしてレスポンスを受け取る所まで行います。
それ以降はパラメータを増やしていくだけなので各々で応用してみてください。

SwiftでChatGPT APIを使うためのコミュニティライブラリがありますが、
今回はAPIをしっかり理解するためにあえて使用していません。

ChatGPT APIの利用料について ※無料クーポンあり

APIでのChatGPT利用は有料となっており従量課金制です。
1000トークンあたり0.002ドルとなっています。

現在は会員登録するだけで3ヶ月間有効な$5分のクーポンが貰えます。
クーポン利用する分には決済情報なども不要です。

短いリクエストやレスポンスであれば100トークン以下で済む事もあるので、
APIを試してみる程度ならば$5分でも結構試す事ができます。

私が試した例を紹介します。

Request:こんにちは
Response:こんにちは!私はAIアシスタントです。何かお手伝いできることがありますか?

このリクエストで8トークン、レスポンスで30トークンの合計38トークンです。
基本的にはどちらも長くなるとトークン数が増えます。
また、応答は毎回異なるのでトークン数はその時によって異なります。
無料分で色々試したい方は気をつけましょう。

APIキーを取得する

まずはOpen AIに登録してAPIキーを取得しましょう。

ユーザーアイコンをクリックするとメニューが出てくるのでView API keysを選択します。
Create new secret keyを押すとAPIキーが作成できます。

一度しか確認出来ないのでしっかりコピーしておきましょう。
一応黒塗りにしていますがSECRET KEYの欄も最初と最後しか表示されません。
なおkeyの作成自体は何度でもできるので忘れても大丈夫です。

keyの管理は厳重に行いましょう。
keyを含めたままパブリックリポジトリにアップロードしてしまうパターンはよく聞きます。
無料分を使っている内は勝手に止まるので問題ありませんが、
有償で使い始めて上限設定を忘れてしまうと勝手に使われて高額請求が来る事があるかもしれません。

またOrganization IDも必要となるのでSettingから確認しておきましょう。

ChatGPT APIを使う

ChatGPT APIの概要

ChatGPT APIはWebAPIです。そのためAIの処理はOpenAIのサーバ上で行われます。
アプリ側から行う事はリクエストを作成してHTTP通信でPOSTしてレスポンスを受け取るだけです。

StableDiffusionなどと異なり自分でモデルを扱う訳でないため、
取扱が非常に楽でマシンスペックも要求されません。

Request

ChatGPT APIのリクエストは以下の通りです。

Authorization: Bearer APIKEY
OpenAI-Organization: ORGID
Content-Type: application/json

headerにAPI KeyとOrganization IDを含めます。
そしてbodyはjsonなのでContent-Typeに指定します。

{
    "model" : "gpt-3.5-turbo"
    ,"messages": [
        {"role": "user", "content": "こんにちは"}
    ]
}

bodyはjsonでパラメータを設定します。
今回は最低限の内容になっています。

modelに使用モデルを設定します。
messagesは配列となっており、中身のroleはユーザーとAIのどちらの発言かでcontentが発言内容になります。

今回は「こんにちは」と送ってAIからの応答を貰うので{“role”: “user”, “content”: “こんにちは”}です。

会話の流れをくんで欲しい時は”role”: “assistant”として過去の会話を含めて送る事が出来ます。

{
    "model" : "gpt-3.5-turbo"
    ,"messages": [
        {"role": "user", "content": "こんにちは"}
        ,{"role": "user", "assistant": "こんにちは!私はAIアシスタントです。何かお手伝いできることがありますか?"}
        ,{"role": "user", "content": "SwiftUIについて教えてください"}
        ,....
    ]
}

この場合は最新のものだけでなく過去の会話分もトークンを消費するので気をつけてください。

Response

レスポンスは以下の通りになります。

{
    "id":"chatcmpl-6wwwh2hM5b6zqpeAEarSytzkrZxeO"
    ,"object":"chat.completion"
    ,"created":1679507631
    ,"model":"gpt-3.5-turbo-0301"
    ,"usage":{"prompt_tokens":8,"completion_tokens":30,"total_tokens":38}
    ,"choices":[
        {
            "message":{
                "role":"assistant"
                ,"content":"こんにちは!私はAIアシスタントです。何かお手伝いできることがありますか?"
            }
            ,"finish_reason":"stop"
            ,"index":0
        }
    ]
}

こちらは項目が多いので今回必要な所だけ抜粋します。

usageにトークン数が入っています。
prompt_tokensが送信したcontentのトークン数、
completion_tokensがAIの返答のcontentのトークン数です。
total_tokensが合計でここから今回の金額が算出できますね。

choicesの中にmessageがあります。ここがAIによる返答です。
形式はリクエストで送ったmessageと同じですね。
リクエストと違って配列でないので単数なので気をつけてくださいね。

swiftからリクエストする

swiftのコードからリクエストして行きます。

リクエスト用のメソッドを作ったので先に全体を掲載します。

private func request() async -> String{
    
    //URL
    guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
        return "URL error"
    }
    
    //URLRequestを作成
    var req = URLRequest(url: url)
    req.httpMethod = "POST"
    req.allHTTPHeaderFields = ["Authorization" : "Bearer \(ChatGPTKey.apiKey)"
                               ,"OpenAI-Organization": ChatGPTKey.orgId
                               ,"Content-Type" : "application/json"]
    req.httpBody = """
{
"model" : "gpt-3.5-turbo"
,"messages": [{"role": "user", "content": "\(content)"}]
}
""".data(using: .utf8)
    
    //URLSessionでRequest
    guard let (data, urlResponse) = try? await URLSession.shared.data(for: req) else {
        return "URLSession error"
    }
    
    //ResponseをHTTPURLResponseにしてHTTPレスポンスヘッダを得る
    guard let httpStatus = urlResponse as? HTTPURLResponse else {
        return "HTTPURLResponse error"
    }
    
    //BodyをStringに、失敗したらレスポンスコードを返す
    guard let response = String(data: data, encoding: .utf8) else {
        return "\(httpStatus.statusCode)"
    }
    print(response)
    
    return response
    
}

それでは内容を順に確認していきましょう。

リクエストの作成

まずはリクエストの作成です。

以下のリクエスト先URLを指定してURLRequestを作成します。
https://api.openai.com/v1/chat/completions

httpMethodにPOSTを設定し、allHTTPHeaderFieldsにDictionary形式でヘッダを設定します。
httpBodyにはひとまずテキストをそのまま用意しcontentだけ変数の中身を埋め込んでいます。

//URL
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
    return "URL error"
}
        
//URLRequestを作成
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.allHTTPHeaderFields = ["Authorization" : "Bearer \(ChatGPTKey.apiKey)"
                                   ,"OpenAI-Organization": ChatGPTKey.orgId
                                   ,"Content-Type" : "application/json"]
urlRequest.httpBody = """
{
"model" : "gpt-3.5-turbo"
,"messages": [{"role": "user", "content": "\(content)"}]
}
""".data(using: .utf8)

リクエストする

HTTPリクエストは非同期で行うためawaitを使用しています。
その為「private func request() async -> String」といったようにメソッドにasyncをつけて非同期のメソッドとしています。

//URLSessionでRequest
guard let (data, urlResponse) = try? await URLSession.shared.data(for: urlRequest) else {
    return "URLSession error"
}

レスポンスを確認する

urlResponseをhttpStatusにキャストして正常にhttpのレスポンスが帰ってきている事を確認します。

レスポンスBodyはjsonなのでひとまずStringにキャストします。

//ResponseをHTTPURLResponseにしてHTTPレスポンスヘッダを得る
guard let httpStatus = urlResponse as? HTTPURLResponse else {
    return "HTTPURLResponse error"
}

//BodyをStringに、失敗したらレスポンスコードを返す
guard let response = String(data: data, encoding: .utf8) else {
    return "\(httpStatus.statusCode)"
}

これでひとまずjsonそのままのデータを確認する事が出来ます。

Viewのコード

今回使うViewはこの様になっています。

気を付ける事はrequestメソッドが非同期なのでTaskで囲ってawatをつけて呼ぶことです。

あとはボタンを連打して余分にリクエストしない様にフラグでボタンを管理しています。
ボタンを有効にする変更はTask内に書かないとレスポンスを待たずにボタンが戻るので注意しましょう。

UI部分はなんでもいいので好みに合わせて変えてください。UIKitでも結構です。

struct ChatGPTRawView: View {
    
    @State private var content = "こんにちは"
    @State private var response = "none"
    @State private var requesting = false

    
    var body: some View {
        NavigationStack {
            VStack {
                VStack {
                    HStack {
                        Text("Massage")
                        Spacer()
                    }
                    
                    TextEditor(text: $content)
                        .frame(height: 100)
                        .padding(5)
                        .border(.black)
                    
                }.padding(.bottom)
                
                
                Button("Request"){
                    requesting = true

                    Task{
                        response = await request()
                        requesting = false
                    }
                }
                .font(.title2)
                .padding()
                .foregroundColor(.white)
                .background(requesting ? .gray :.blue)
                .cornerRadius(20)
                .disabled(requesting)
                
                
                VStack {
                    HStack {
                        Text("Response")
                        Spacer()
                    }
                    
                    TextEditor(text: $response)
                        .padding(5)
                        .border(.black)
                    
                }.padding(.top)
                
                Spacer()
                
            }
            .padding()
            .navigationTitle(Text("ChatGPT"))
        }
    }
    
    
    private func request() async -> String{
    //略
    }
}

json用の構造体を用意する

jsonをそのまま確認できる様になりましたが、自前でパーサを用意するのは面倒です。

SwiftにはCodableというものがあります。
これを実装する事で簡単に構造体をjson形式にEncode/Decodeする事が出来ます。

リクエスト構造体

リクエスト用の構造体を用意してみましょう。

struct ChatGPTRequest:Codable{
    let model:String
    let messages:[ChatGPTMassage]
}

struct ChatGPTMassage:Codable{
    let role:String
    let content:String
}

jsonの項目に合わせて変数名と型を定義します。
messagesが入れ子の構造となっているので別な構造体に分けます。
構造体の配列になっていますが中身もCodableなので特に気にせず見たまま実装すばOKです。

リクエストする際は構造体に必要なデータを入れてJSONEncoderでencodeすればOKです。

let message = ChatGPTMassage(role: "user", content: content)
let request = ChatGPTRequest(model: "gpt-3.5-turbo", messages: [message])
let requestData try? JSONEncoder().encode(req)

これでhttpBodyに入れて送るデータができました。

レスポンス構造体

次はレスポンスを構造体に入れて使いやすくします。

struct ChatGPTResponse:Codable{
    let id:String
    let object:String
    let created:Int
    let model:String
    let usage:ChatGPTUsage
    let choices:[ChatGPTChoices]
}

struct ChatGPTUsage:Codable{
    let prompt_tokens:Int
    let completion_tokens:Int
    let total_tokens:Int
}

struct ChatGPTChoices:Codable{
    let message:ChatGPTMassage
    let finish_reason:String
    let index:Int
}

数値型が混ざりますが、使う値に合わせて型を指定すればOKです。
今回は整数値しか使っていないのでIntとしました。

messageにはリクエストで使ったのと同じ構造体を使っています。

構造体に収める時は以下のコードになります。
dataはURLSessionで受け取ったデータをそのまま渡すだけです。

let chatGPTResponse = try? JSONDecoder().decode(ChatGPTResponse.self, from: data)

これでリクエスト/レスポンスのデータを簡単に扱える様になりました。
今回は最低限の項目で行っていますが、必要なパラメータが増えた場合は適宜増やして使ってください。

Codableな構造体を使うように変更する

先程のコードを以下の様に変更します。

private func request() async throws -> ChatGPTResponse{
    
    //URL
    guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
        throw NSError(domain: "URL error", code: -1)
    }
    
    //URLRequestを作成
    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = "POST"
    urlRequest.allHTTPHeaderFields = ["Authorization" : "Bearer \(ChatGPTKey.apiKey)"
                               ,"OpenAI-Organization": ChatGPTKey.orgId
                               ,"Content-Type" : "application/json"]
    urlRequest.httpBody = requestBody
    
    //URLSessionでRequest
    guard let (data, urlResponse) = try? await URLSession.shared.data(for: urlRequest) else {
        throw NSError(domain: "URLSession error", code: -1)
    }
    
    //ResponseをHTTPURLResponseにしてHTTPレスポンスヘッダを得る
    guard let httpStatus = urlResponse as? HTTPURLResponse else {
        throw NSError(domain: "HTTPURLResponse error", code: -1)
    }
    
    //BodyをStringに、失敗したらレスポンスコードを返す
    guard let response = String(data: data, encoding: .utf8) else {
        throw NSError(domain: "\(httpStatus.statusCode)", code: -1)
    }
    print(response)
    
    //JSONをChatGPTResponse構造体にする
    guard let chatGPTResponse = try? JSONDecoder().decode(ChatGPTResponse.self, from: data) else {
        throw NSError(domain: response, code: -1)
    }
    
    return chatGPTResponse
    
}

変更した部分の解説

変更内容を上から順に確認していきましょう。

まずはrequestメソッドでChatGPTResponseを受け取る様に変えます。

private func request() async throws -> ChatGPTResponse{
//略
}

エラーはthrowを使うように変更します。
以下の様にエラー部分を書き換えていきます。

//URL
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
    throw NSError(domain: "URL error", code: -1)
}
...

httpBodyは以下の様にプロパティを作り、構造体を作りエンコードしたものを返します。

private var requestBody:Data?{
    let message = ChatGPTMassage(role: "user", content: content)
    let req = ChatGPTRequest(model: "gpt-3.5-turbo", messages: [message])
    return try? JSONEncoder().encode(req)
}

//
urlRequest.httpBody = requestBody

レスポンスはdetaをdecodeしてそのままChatGPTResponseにして返します。

guard let chatGPTResponse = try? JSONDecoder().decode(ChatGPTResponse.self, from: data) else {
    throw NSError(domain: response, code: -1)
}
        
return chatGPTResponse

それに合わせてボタンのrequestメソッド呼び出しも変更した形になります。

Button("Request"){
    requesting = true

    Task{
        do{
            let chatGptResponse = try await request()
            response = chatGptResponse.choices.first?.message.content ?? "no message"
        }catch{
            let nsError = error as NSError
            response = nsError.domain
        }
        requesting = false
    }
}

コード全文

struct ChatGPTView: View {
    
    @State private var content = "こんにちは"
    @State private var response = "none"
    @State private var requesting = false

    
    var body: some View {
        NavigationStack {
            VStack {
                VStack {
                    HStack {
                        Text("Massage")
                        Spacer()
                    }
                    
                    TextEditor(text: $content)
                        .frame(height: 100)
                        .padding(5)
                        .border(.black)
                    
                }.padding(.bottom)
                
                
                Button("Request"){
                    requesting = true

                    Task{
                        do{
                            let chatGptResponse = try await request()
                            response = chatGptResponse.choices.first?.message.content ?? "no message"
                        }catch{
                            let nsError = error as NSError
                            response = nsError.domain
                        }
                        requesting = false
                    }
                }
                .font(.title2)
                .padding()
                .foregroundColor(.white)
                .background(requesting ? .gray :.blue)
                .cornerRadius(20)
                .disabled(requesting)
                
                
                VStack {
                    HStack {
                        Text("Response")
                        Spacer()
                    }
                    
                    TextEditor(text: $response)
                        .padding(5)
                        .border(.black)
                    
                }.padding(.top)
                
                Spacer()
                
            }
            .padding()
            .navigationTitle(Text("ChatGPT"))
        }
    }
    
    
    private var requestBody:Data?{
        let message = ChatGPTMassage(role: "user", content: content)
        let req = ChatGPTRequest(model: "gpt-3.5-turbo", messages: [message])
        return try? JSONEncoder().encode(req)
    }

    
    private func request() async throws -> ChatGPTResponse{
        
        //URL
        guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
            throw NSError(domain: "URL error", code: -1)
        }
        
        //URLRequestを作成
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = "POST"
        urlRequest.allHTTPHeaderFields = ["Authorization" : "Bearer \(ChatGPTKey.apiKey)"
                                   ,"OpenAI-Organization": ChatGPTKey.orgId
                                   ,"Content-Type" : "application/json"]
        urlRequest.httpBody = requestBody
        
        //URLSessionでRequest
        guard let (data, urlResponse) = try? await URLSession.shared.data(for: urlRequest) else {
            throw NSError(domain: "URLSession error", code: -1)
        }
        
        //ResponseをHTTPURLResponseにしてHTTPレスポンスヘッダを得る
        guard let httpStatus = urlResponse as? HTTPURLResponse else {
            throw NSError(domain: "HTTPURLResponse error", code: -1)
        }
        
        //BodyをStringに、失敗したらレスポンスコードを返す
        guard let response = String(data: data, encoding: .utf8) else {
            throw NSError(domain: "\(httpStatus.statusCode)", code: -1)
        }
        print(response)
        
        //JSONをChatGPTResponse構造体にする
        guard let chatGPTResponse = try? JSONDecoder().decode(ChatGPTResponse.self, from: data) else {
            throw NSError(domain: response, code: -1)
        }
        
        return chatGPTResponse
        
    }
}

最後に

今回はChatGPT APIを使ってみました。
AI自体をローカルで稼働させる訳ではないので、
この程度の内容であればコミュニティライブラリを使用せずとも簡単に利用出来ます。

また、httpリクエストを行い、json形式でレスポンスがあるというのはWeb APIではよくあるパターンです。
async/awaitを使ったhttp通信と、Codableによるjsonのencode/decodeを覚えておくと、
APIに変更があった場合や他のWeb APIを使うときにもすぐに対応できるようなります。

コメント

タイトルとURLをコピーしました