ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift UI와 Gemini API를 활용한 Chat Bot: 기능 구현 (1/2)
    공부/iOS 2024. 3. 13. 00:21

    목차

    • Gemini API?
    • Google API KEY 발급하기
    • Gemini SDK 설치하기
    • Gemini API의 4가지 기능
    • 기능 구현하기

     

    Gemini API?

    제미나이는 구글과 딥마인드가 개발한 멀티모달 생성형 인공지능 모델입니다. 이 포스트에서는 제미니 API의 주요 기능과 스위프트 앱에서 활용하는 방법에 대해 살펴보겠습니다.

     

    우선, 제미나이 API는 두 가지 형태의 개발 환경을 통해 제공됩니다.

    • 구글 AI 제미나이 API(Google AI Gemini API)
    • 버텍스 AI 제미나이 API(Vertex AI Gemini API)

    이 중 구글 AI 제미나이 API는 학습 목적이나 소규모 개발에 적합합니다. 이에 반해 버텍스 AI 제미나이 API는 중규모 이상의 시스템에 적합하며 구글 클라우드 서비스인 버텍스 AI와 연동하여 사용할 수 있습니다. 

     

    이 포스트에서 다뤄볼 것은 "Google AI Gemini API"으로 앞으로는 이걸 "제미나이 API"라고 부르겠습니다.

     

    Google API KEY 발급하기

    우선 ai.google.dev 에서 API 키를 발급받아 줍니다.

    1. [Get API key in Google AI Studio] 
    2. 이용 약관 체크 + [Continue
    3. 좌측 상단의 [Get API Key]
    4. [Create API key in new project] 
    5. API 키 복사

    그런 뒤, Xcode 프로젝트에서 GenerativeAI-Info.plist 파일을 만들어 API 키를 저장해줍니다.

    잘못 노출이 되면 과금이 될 수도 있으니 조심해야합니다.

    API를 관리할 수 있도록 APIKey 파일을 추가합니다.

    import Foundation
    
    enum APIKey {
        // Fetch the API key from `GenerativeAI-Info.plist`, ``:예약어
        static var `default`: String {
            guard let filePath = Bundle.main.path(forResource: "GenerativeAI-Info", ofType: "plist") else {
                fatalError("Couldn't find file 'GenerativeAI-Info.plist'.")
            }
            
            let plist = NSDictionary(contentsOfFile: filePath)
            guard let value = plist?.object(forKey: "API_KEY") as? String else {
                fatalError("Couldn't find key 'API_KEY' in 'GenerativeAI-Info.plist'.")
            }
            
            if value.starts(with: "_") || value.isEmpty {
                fatalError(
                    "Follow the instructions at https://ai.google.dev/tutorials/setup to get an API key."
                )
            }
            
            return value
        }
    }
    // https://github.com/google/generative-ai-swift/blob/main/Examples/GenerativeAISample/APIKey/APIKey.swift

    Gemini SDK 설치하기

    SPM을 활용해 패키지를 추가해줍니다.

    https://github.com/google/generative-ai-swift.git

    Gemini API의 4가지 기능

    Gemini API는 4가지의 기능을 제공합니다.

    현재 구글에선 gemini-pro-vision(멀티 모달)와 gemini-pro(텍스트 모델)을 제공하고 있습니다.

    만약 멀티턴 기능이 필요하다면 텍스트 모델을 사용해야겠죠.

    각 기능에 대해선 https://ai.google.dev/tutorials/swift_quickstart?hl=ko 에서 더 자세히 확인하실 수 있습니다.

     

    기능 구현하기

    위의 기능 중, 스트리밍을 곁들인 이미지 멀티 모달 멀티턴 대화 기능에 대해 소개하겠습니다.

    user가 흰색 말풍선으로 질문을 하면, model이 노란색 말풍선으로 답변하는 방식입니다.

    이미지의 경우, 하단 PhotosPicker 버튼으로 "사진"에서 이미지를 가지고 오며,

    전송된 이미지는 LazyHStack으로 스크롤하여 보여주겠습니다.

     

     

    1. 모델을 추가

    import Foundation
    
    enum ChatRole {
        case user
        case model
    }
    
    struct ChatMessage: Identifiable, Equatable {
        let id = UUID().uuidString
        var role: ChatRole
        var message: String
        var images: [Data]?
    }

    2. 멀티턴 채팅 기능

    • 짧은 대화의 느낌을 주고자 최대 토큰 수를 100개로 제한했으며(optional), gemini-pro 모델을 사용했습니다.
    • 멀티턴을 위해 startChat(history: history)를 사용해 이전 대화 목록을 모델에 제공합니다.
    • 답변을 스트리밍 방식으로 받기 위해 sendMessageStream를 사용해 메세지를 전송합니다.
    • 응답이 들어올 때 마다 마지막 메세지(model의 답변)에 덧붙여 사용자에게 보여줍니다.
    • 답변이 끝난 경우에는 history에 이전 대화 기록을 추가하고 loadingResponse를 false로 만들어줍니다.
    //MARK: - 유저 메세지, AI 메세지를 리스트에 추가
    messages.append(.init(role: .user, message: message, images: imageData))	// 내 질문
    messages.append(.init(role: .model, message: "", images: nil))			// 모델 답변
    
    do {
        //MARK: - Multi-turn 대화 with streaming
        let config = GenerationConfig(maxOutputTokens: 100)
        let chatModel = GenerativeModel(name: "gemini-pro", apiKey: APIKey.default, generationConfig: config)
    
        let chat = chatModel.startChat(history: history)
        let responseStream = chat.sendMessageStream(message)
        for try await chunk in responseStream {
            guard let text = chunk.text else { return }
    
            let lastChatMessageIndex = messages.count - 1
            messages[lastChatMessageIndex].message += text
        }
    
        history.append(.init(role: "user", message))
        history.append(.init(role: "model", parts: messages.last?.message ?? ""))
        loadingResponse = false
    }

    3. 이미지 멀티 모달 기능

    • gemini-pro-vision 모델을 활용해 텍스트 및 이미지를 처리합니다.
    • 모델에 입력 토큰의 한도(4MB)를 지킬 수 있도록 Data compression을 0.1로 진행했습니다.
    • 스트리밍을 사용하기 위해 generateContentStream으로 AsyncThrowingStream을 만들어 줍니다.
    • 응답이 들어올 때 마다 마지막 메세지(model의 답변)에 덧붙여 사용자에게 보여줍니다.
    • 답변이 종료된 경우에는 loadingResponse를 false로 바꿔줍니다.
    //MARK: - 유저 메세지, AI 메세지를 리스트에 추가
    messages.append(.init(role: .user, message: message, images: imageData))	// 내 질문
    messages.append(.init(role: .model, message: "", images: nil))			// 모델 답변
    
    do {
        //MARK: - 이미지 입력으로 텍스트 생성(multimodal)
        let chatModel = GenerativeModel(name: "gemini-pro-vision", apiKey: APIKey.default)
    
        var images = [PartsRepresentable]()
        for data in imageData {
            // 이미지 최대 크기를 4MB로 제한하기 위해 Data compression 진행
            if let compressedData = UIImage(data: data)?.jpegData(compressionQuality: 0.1) {
                images.append(ModelContent.Part.jpeg(compressedData))
            }
        }
    
        // Request and stream the response
        let contentStream = chatModel.generateContentStream(message, images)
        for try await chunk in contentStream {
            guard let text = chunk.text else { return }
    
            let lastChatMessageIndex = messages.count - 1
            messages[lastChatMessageIndex].message += text
        }
        loadingResponse = false
    }

    4. 오류 처리

    • 요청에 오류가 발생한 경우 오류 메세지를 출력합니다.
    catch {
        loadingResponse = false
        messages.removeLast()
        messages.append(.init(role: .model, message: "다시 시도해주세요."))
        print(error.localizedDescription)
    }

    5. ChatService 

    • 앱 내부에서 상태를 확인할 수 있도록 Observable macro로 변수를 선언해줍니다.
    • 비동기 처리를 위해 async 함수를 만들어줍니다.
    • 코드 전문은 "더보기"
    더보기
    import SwiftUI
    import GoogleGenerativeAI
    
    @Observable
    class ChatService {
        private(set) var messages = [ChatMessage]()
        private(set) var history = [ModelContent]()
        private(set) var loadingResponse = false
        
        func sendMessage(message: String, imageData: [Data]) async {
            
            loadingResponse = true
            
            //MARK: - 유저 메세지, AI 메세지를 리스트에 추가
            messages.append(.init(role: .user, message: message, images: imageData))
            messages.append(.init(role: .model, message: "", images: nil))
            
            do {
                if imageData.isEmpty {
                    //MARK: - Multi-turn 대화 with streaming
                    let config = GenerationConfig(maxOutputTokens: 100)
                    let chatModel = GenerativeModel(name: "gemini-pro", apiKey: APIKey.default, generationConfig: config)
    
                    let chat = chatModel.startChat(history: history)
                    let responseStream = chat.sendMessageStream(message)
                    for try await chunk in responseStream {
                        guard let text = chunk.text else { return }
                        
                        let lastChatMessageIndex = messages.count - 1
                        messages[lastChatMessageIndex].message += text
                    }
                    
                    history.append(.init(role: "user", message))
                    history.append(.init(role: "model", parts: messages.last?.message ?? ""))
                    loadingResponse = false
                    
                } else {
                    //MARK: - 이미지 입력으로 텍스트 생성(multimodal)
                    let chatModel = GenerativeModel(name: "gemini-pro-vision", apiKey: APIKey.default)
                    
                    var images = [PartsRepresentable]()
                    for data in imageData {
                        // 이미지 최대 크기를 4MB로 제한하기 위해 Data compression 진행
                        if let compressedData = UIImage(data: data)?.jpegData(compressionQuality: 0.1) {
                            images.append(ModelContent.Part.jpeg(compressedData))
                        }
                    }
                    
                    // Request and stream the response
                    let contentStream = chatModel.generateContentStream(message, images)
                    for try await chunk in contentStream {
                        guard let text = chunk.text else { return }
                        
                        let lastChatMessageIndex = messages.count - 1
                        messages[lastChatMessageIndex].message += text
                    }
                    
                    loadingResponse = false
                }
            }
            catch {
                loadingResponse = false
                messages.removeLast()
                messages.append(.init(role: .model, message: "다시 시도해주세요."))
                print(error.localizedDescription)
            }
        }
    }

    화면 개발

    분량 초과 관계로 다음 포스트에서 이어서 하겠습니다...

     

    Reference

Designed by Tistory.