-
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 키를 발급받아 줍니다.
- [Get API key in Google AI Studio]
- 이용 약관 체크 + [Continue]
- 좌측 상단의 [Get API Key]
- [Create API key in new project]
- 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가지의 기능을 제공합니다.
- 텍스트 전용 입력에서 텍스트 생성 : 텍스트 입력으로 질문을 하면 답변(이전 대화 기억 못함)
- 텍스트 및 이미지 입력에서 텍스트 생성 (multi modal) : 이미지 + 텍스트 입력으로 질문을 하면 답변(이전 대화 기억 못함)
- 멀티턴 대화 만들기 (채팅) : 이전 대화를 이어서 할 수 있음
- 스트리밍을 사용하여 더 빠르게 상호작용하기 : 긴 답변을 쪼개서 여러번에 받을 수 있어 빠르게 상호작용이 가능
현재 구글에선 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
- ai-google-dev: https://ai.google.dev/tutorials/swift_quickstart?hl=ko
- generative-ai-swift example: https://github.com/google/generative-ai-swift/tree/main/Examples
- youtube: https://www.youtube.com/watch?v=4SeW1x4m6Gc
- wikidocs: https://wikidocs.net/book/14285
'공부 > iOS' 카테고리의 다른 글
Swift UI와 Gemini API를 활용한 Chat Bot: 채팅 구현 (2/2) (0) 2024.03.19 [SwiftUI] 1. TCA를 왜 도입하는걸까? (MVVM의 운명은?) (0) 2024.02.27 [iOS] status code가 200일 때 빈 응답이 들어오는 경우 (0) 2024.02.17 [iOS]텍스트필드가 등장할 때 타임아웃 뜨는 버그 해결법 (2) 2024.01.17 키체인을 활용해 jwt Token 안전하게 저장하기 (1) 2024.01.16