AI의 시대에 살고 있다해도 과언이 아닌 요즘 세상
우리는 얼마나 그것들을 이해하고 사용하고 있는가
Gemini API를 사용하여 AI를 직접 구현해봄으로서
AI에 대해 체험해보고 한 발짝 더 알아가 보겠다.
Gemini API를 사용하여 AI 챗 봇 앱 만들기
이번에 만들 챗 봇 앱의 동작 영상이다.
먼저, 메인 함수 부터 구성해 보겠다.
[MainActivity.kt]
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val chatViewModel = ViewModelProvider(this)[ChatViewModel::class.java]
setContent {
ChatBotTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ChatPage(modifier = Modifier.padding(innerPadding), chatViewModel)
}
}
}
}
}
프로젝트 생성시 기본으로 구성되는 코드에서
Scaffold 부분에 ChatPage()를 추가해준다.
(Scaffold는 기본 레이아웃을 구성, Modifier는 UI요소의 모양, 배치, 동작 등을 수정하고 구성하는데 사용되는 객체)
chatViewModel은 API 키를 이용한 모델 생성 및 메시지 보내기 기능과 메시지 리스트 관리 등을 할 객체가 될 것으로 아래에서 자세히 보겠다.
내부 padding을 적용하는 ChatPage를 만들겠다.
[ChatPage.kt]
@Composable
fun ChatPage(modifier: Modifier = Modifier, viewModel: ChatViewModel) {
Column(
modifier = modifier
) {
AppHeader()
MessageList(
modifier = Modifier.weight(1f),
messageList = viewModel.messageList
)
MessageInput(
onMessageSend = {
viewModel.sendMessage(it)
}
)
}
}
@Composable 어노테이션을 사용하여 함수를 만들어준다.
수직으로 UI 요소를 배열하는 레이아웃인 Column을 정의해주고, modifier를 Column에 전달하여 ChatPage 함수 호출시 지정된 Modifier를 적용해준다.
챗 페이지의 구성은 타이틀에 해당하는 AppHeader, 메시지 리스트, 메시지 입력란 정도가 되며, 메시지 내용은 ChatViewModel을 통해 관리될 것이다.
UI 상단 헤더에 해당하는 AppHeader() 함수를 선언해주고, 취향에 맞게 디자인한다.
[ChatPage.kt]
@Composable
fun AppHeader() {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
) {
Text(
modifier = Modifier.padding(16.dp),
text = "SuldenLion's Chat Bot",
color = Color.White,
fontSize = 22.sp
)
}
}
다음은 화면 하단의 메시지를 입력하는 함수이다.
[ChatPage.kt]
@Composable
fun MessageInput(onMessageSend : (String)-> Unit) {
var message by remember {
mutableStateOf("")
}
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
modifier = Modifier.weight(1f),
value = message,
onValueChange = {
message = it
}
)
IconButton(onClick = {
if(message.isNotEmpty()){
onMessageSend(message)
message = ""
}
}) {
Icon(
imageVector = Icons.Default.Send,
contentDescription = "Send"
)
}
}
}
Column과 반대로 수평으로 UI 요소를 배열하는 Row 레이아웃을 적용시켜주고 padding과 vertical 옵션을 준다.
텍스트 필드(OutlinedTextField)를 만든 후, remember로 선언한 message 변수가 onChange 발생할 때, 즉 텍스트가 입력받아질때 새로운 값으로 변수가 업데이트 되게한다. (remember 함수는 Composable 함수가 재구성 될때 상태를 유지하기 위해 사용되는 함수. 'mutableStateOf'와 함께 사용되어 상태가 변경되면 UI를 자동으로 다시 렌더링해줌)
weight 옵션은 텍스트 필드에 긴 문장이 들어왔을 경우 컴포넌트가 깨지는 것을 방지하기 위함이다.
전송을 위한 버튼을 onClick 이벤트와 함께 만들어준다.
메시지가 비어있지 않은 경우, 해당 메시지를 보내고 message 변수는 다음 입력을 위해 flush 해주겠다.
Icon은 매개변수로 imageVector라는 이미지 값과 description을 갖는데, 기본으로 제공하는 아이콘을 위와 같이 써주면 된다.
onMessageSend(message)를 구현하기 위해서 뷰 모델을 만들어 주겠다.
[ChatViewModel.kt]
class ChatViewModel : ViewModel() {
val messageList by lazy {
mutableStateListOf<MessageModel>()
}
val generativeModel : GenerativeModel = GenerativeModel(
modelName = "gemini-pro",
apiKey = Constants.apiKey
)
fun sendMessage(question : String) {
viewModelScope.launch {
try {
val chat = generativeModel.startChat(
history = messageList.map {
content(it.role) {text(it.message)}
}.toList()
)
messageList.add(MessageModel(question, "user"))
messageList.add(MessageModel("Typing...", "model"))
val response = chat.sendMessage(question)
messageList.removeLast()
messageList.add(MessageModel(response.text.toString(), "model"))
} catch (e: Exception) {
messageList.removeLast()
messageList.add(MessageModel("Error : " + e.message.toString(), "model"))
}
}
}
}
먼저, AI 사용을 위한 API 세팅부터 하겠다.
인터넷에 'gemini sdk android'를 검색 후 아래 사이트에 들어간다.
Gemini API Docs 페이지를 찾아가준 후
스크롤을 내리다 보면 sdk 사용을 위한 dependency가 있는데,
해당 코드를 복사 후, 프로젝트 내의 build.gradle.kts/dependencies 항목에 붙여넣기 해주고 Sync 해준다.
다시 모델로 돌아와서 아래 함수를 정의해준다.
GenerativeModel 함수를 상속받는 위 함수는 모델명과 key값을 Argument로 가지는데, Key값은 Constants.kt 파일을 새로 생성하여 따로 관리해 주겠다.
API key는 사이트에서 Google AI Studio 사이트에서 Get API Key를 찾아 들어가면, 아래의 화면이 나오는데 Create an API Key를 클릭하면 발급된다.
(※ Key값은 타인에게 노출되어선 안되며, API 사용량에 따라 요금이 부과될 수 있다)
프로젝트에 Key값을 세팅해보겠다.
[Constants.kt]
object Constants {
val apiKey = "Your_own_API_Key"
}
이제 Gemini API를 쓸 준비가 완료되었다.
Model의 sendMessage()로 돌아와서
함수 초입에 viewModelScope.launch를 선언해준다.
이는 코루틴을 사용하여 비동기 작업을 수행해 주는 함수로, ViewModel이 활성화 되어 있는 동안 실행된다.
ViewModel이 클린업 될 때, 이 코루틴도 자동으로 취소되며, 이를 통해 메모리 누수를 방지하고, UI 관련 비동기 작업을 안전하게 수행할 수 있다고 한다.
Model의 startChat()으로 chat 객체를 초기화한 후, 리스트의 각 항목을 content로 감싸서 history로 보관한다.
리스트에 추가할 요소는 String 형태의 '메시지와 역할'을 매개변수로 갖는 MessageModel 객체를 만들고, 각각 추가해준다.
[MessageModel.kt]
data class MessageModel(
val message : String,
val role : String,
)
그 다음 chat.sendMessage(question)으로 모델의 응답을 받는다.
리스트의 removeLast()로 메시지 생성 중임을 보여주던 'Typing...'을 제거하고 응답을 리스트에 추가해준다.
예외 발생 시에는 'Typing...' 메시지를 지우고 에러 로그를 띄운다. (인터넷 연결 안됐을때의 채팅 등에서 에러를 띄우고, 정치나 테러등의 민감 이슈에 대해서 가끔 에러를 띄우기도 함)
ChatPage.kt 파일로 돌아와서 MessageList를 정의해 보겠다.
[ChatPage.kt]
@Composable
fun MessageList(modifier: Modifier = Modifier,messageList : List<MessageModel>) {
if(messageList.isEmpty()){
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
modifier = Modifier.size(60.dp),
painter = painterResource(id = R.drawable.baseline_question_answer_24),
contentDescription = "Icon",
tint = Purple80,
)
Text(text = "Ask me anything", fontSize = 22.sp)
}
}else{
LazyColumn(
modifier = modifier,
reverseLayout = true
) {
items(messageList.reversed()){
MessageRow(messageModel = it)
}
}
}
}
메시지가 없는 초기화면 일 때, modifier를 fillMaxSize() 즉, Column이 화면의 최대 크기를 차지하게 한다.
그리고 수평, 수직 중앙 정렬을 시켜준다.
그리고 아이콘을 중앙에 배치 시켜줄건데, 프로젝트의 res/drawable에 Vector Asset을 생성할 것이다.
아래 화면에서 Clip art를 더블 클릭해준 후, 적당한 Icon을 선택해준다.
Icon에 대한 세팅과 텍스트까지 세팅해주면
아래와 같은 화면을 볼 수 있다.
그리고 메시지 리스트가 비어있지 않은 경우에는 LazyColumn을 채팅 보여주는 항목으로 사용한다. (RecyclerView와 기능이 유사함)
reverseLayout 속성을 true로 주고, messageList.reversed()를 해줌으로써 채팅이 아래에서부터 위로 올라가는 것으로 보이게 해준다.
messageRow(messageModel = it)은 현재 메시지를 MessageRow에 전달하는 역할을 할 것이다.
MessageRow 함수를 구성해보겠다.
[ChatPage.kt]
@Composable
fun MessageRow(messageModel: MessageModel) {
val isModel = messageModel.role=="model"
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.align(if (isModel) Alignment.BottomStart else Alignment.BottomEnd)
.padding(
start = if (isModel) 8.dp else 70.dp,
end = if (isModel) 70.dp else 8.dp,
top = 8.dp,
bottom = 8.dp
)
.clip(RoundedCornerShape(48f))
.background(if (isModel) ColorModelMessage else ColorUserMessage)
.padding(16.dp)
) {
SelectionContainer {
Text(
text = messageModel.message,
fontWeight = FontWeight.W500,
color = Color.White
)
}
}
}
}
}
메시지가 보일 화면의 구성, 즉 말풍선에 해당하는 부분에 대한 코드이다.
isModel이라는 boolean 변수를 두어 말풍선의 위치를 달리하고, 내부의 Box 객체를 말풍선으로서 사용한다. (외부 Box는 전체를 담을 레이아웃)
조건에 따라 좌우, 배경색 설정을 해주고 clip() 함수로 모서리를 둥글게 해준다.
또한, SelectionContainer를 통해 Box 안에 텍스트가 배치될 수 있도록 해주고 텍스트에 대한 속성을 적당히 세팅해준다.
background의 ColorModelMessage나 ColorUserMessage는 미리 매크로로 지정한 Color 값이다. (ui.theme 아래의 Color.kt)
여기까지 코드를 짰다면 프로그램이 잘 작동할 것이다.
'App 소개' 카테고리의 다른 글
카카오 맵 API를 활용한 경로 표시 어플리케이션 (1) | 2024.10.25 |
---|---|
3D Paint App (0) | 2024.06.23 |
심심해서 만들어보는 앱 "SuldenLion's Versatile App" 개발로그_3 (0) | 2024.01.14 |
심심해서 만들어보는 앱 "SuldenLion's Versatile App" 개발로그_2 (0) | 2024.01.07 |
심심해서 만들어보는 앱 "SuldenLion's Versatile App" 개발로그_1 (0) | 2023.12.31 |
댓글