[Swift] 31 ChatGPTアプリ製作 その2 HTTPRequest

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

GPT-3.5のAPIとやりとりできるようになりました。

ChatGPTの力を借り色々試行錯誤しながらで約半日掛かりました。取りあえず送受信を1往復できるようにしています。

チャットのように交互に表示させるのは難しいため、上下段に振り分けました。

次回以降、複数回の送受信に対応させます。

OpenAIがiOS, iPadOSアプリを5/26にリリースしているので、比較しながら機能を増やしていきたいです。

iOS版
iPadOS版
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(sortDescriptors:[])
    private var pairs: FetchedResults<Interaction>

    var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(pairs) { pair in
                        NavigationLink{
                            if((pair.instruction?.isEmpty) == false){
                                Draft(text:pair.instruction!, interaction: pair)
                            }
                        }
                        label:{
                            if((pair.instruction?.isEmpty) == false){
                                Text(pair.instruction!)
                            }
                        }
                    }

                    .onDelete(perform: deleteInteraction)
                }
                .navigationTitle("ChatGPT リクエスト")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar{
                    // 新規リクエスト作成
                    ToolbarItem(placement:.navigationBarTrailing){
                        NavigationLink{
                            Draft()
                        }label:{
                            Text("+")
                        }
                    }
                }

                List {
                    ForEach(pairs, id: \.self) { pair in
                        if let res = pair.res, !res.isEmpty {
                            Text(res)
                                .foregroundColor(.white)
                                .background(Color.blue)
                        }
                    }


                    .onDelete(perform: deleteInteraction)
                }

                Button(action:{
                    if pairs.last != nil{
                        sendRequest()
                    }else{
                        print("pairsは空です")
                        
                    }
                }){
                    Text("送信")
                    .font(.system(size: 24))
                }
            }
        }
    }

    func deleteInteraction(offsets:IndexSet){
        for offset in offsets{
            viewContext.delete(pairs[offset])
        }
            
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }
    }

    func sendRequest(){
        let urlAPI = "https://api.openai.com/v1/chat/completions";
        let apiKey = "API key";
        let model = "gpt-3.5-turbo"
        let systemStr: String = "あなたは根拠が明確に存在することのみ発言するチャットボットです。"

        let authHeader = "Bearer \(apiKey)"
        var headers = [String: String]()
        headers["Authorization"] = authHeader
        headers["Content-Type"] = "application/json"

        guard let url = URL(string: urlAPI) else {
            fatalError("Invalid URL")
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers

        // requestData作成・送受信
        if let lastInteraction = pairs.last {
            if let instruction:String? = lastInteraction.instruction {
                print("instruction:")
                print(instruction)

                let requestData:String = "{\"model\":\"\(model)\", \"messages\":[{\"role\":\"system\",\"content\":\"\(systemStr)\"},{\"role\":\"user\",\"content\":\"\(instruction!)\"}], \"temperature\":0.0}";
                print(requestData)
                
                var responseData: [String: Any] = [:]
                responseData = sendHTTPRequest(url: url, headers: headers, requestData: requestData)
                
                print(responseData)

                if let choices = responseData["choices"] as? [[String: Any]],
                let message = choices.first?["message"] as? [String: Any],
                var content = message["content"] as? String {
                    print(content)
                    lastInteraction.res = content
                }

            } else {
                print("instruction is nil")
                return
            }
        } else {
            print("pairs is empty")
            return
        }
    }

    func sendHTTPRequest(url: URL, headers: [String: String], requestData: String) -> [String: Any] {
        var responseData: [String: Any] = [:]
        var timeoutBool = false

        let semaphore = DispatchSemaphore(value: 0)

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers
        request.httpBody = requestData.data(using: .utf8) // requestDataをData型に変換

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                timeoutBool = true
                print("HTTP request failed: \(error)")
                return
            }

            if let data = data {
                do {
                    responseData = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
                } catch {
                    print("Failed to parse response data: \(error)")
                }
            }

            semaphore.signal()
        }

        task.resume()
        _ = semaphore.wait(timeout: DispatchTime.now() + 90)

        if timeoutBool {
            <中略>
            return [:]
        }

        return responseData
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Core Data(Data Model)の内容

[Swift] 30 関数定義にあるアンダースコアの意味

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

前から関数の定義で引数の所にあるアンダースコアが気になっていたので調べてみました。

アンダースコアを付けると関数を使用する際に引数ラベルを省略できるそうです。これには驚きました。普通はアンダースコアなしでも引数ラベルはいらないものですが。

これのおかげでコードの見栄えがどっちにしても悪くなるんですが、Appleにすればどうでもいいようです。

ハードやOSの見た目にはこだわるのに、開発者にだけ見える部分には無頓着ですね。

# 引数にvalueを必ず付ける
func intToString(value: Int) -> String {
    return String(value)
}

print(intToString(value:1))

# 引数にvalueを付けなくてもよい
func intToString2(_ value: Int) -> String {
    return String(value)
}

print(intToString2(1))

出力
----------
1
1
----------

[Xcode] printデバッグ時のコンソール表示

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

Xcodeの独特なUIのおかげで足止めを食らったので記録しておきます。

何かの拍子にDebug AreaのVariables Viewボタンを押してしまったためにコンソールが表示できず、しばらく右往左往しました。

Debug Area右下のView選択ボタンにホバーした際、ボタンを押した時の動作がポップアップ表示されるのですが、非常に紛らわしいです。自分なら今現在の状態、例えば”Console Active”と表示させます。ボタンのラベルならともかく、ポップアップにボタン動作を表示させるのは私的には御法度です。

青色だからActiveだというのは分からないことはないものの、どうもしっくりきません。またデフォルトでは左右表示ですから元に戻すボタンが欲しいところです。

高機能ゆえに仕方がないことかもしれませんが、本当に初心者泣かせなUIです。この分かりにくさはDAWアプリのCubaseに通じるところがありますね。

IDEを使うとどうしても不満たらたらになってしまいます。iOSアプリを非IDE環境で作るのはハードルが高すぎるので我慢するしかありません。

Variables Viewのみ表示
Variables Viewとコンソール表示