Skip to content

Commit

Permalink
Implement open ai service (#13)
Browse files Browse the repository at this point in the history
* feat: implement CertifyMissionRequest

* feat: implement CertifyMissionResponse

* chore: change local.properties keys

* feat: implement OpenAiApiService

* chore: rename OpenAi to OpenAI

* feat: add openAIApiService into ServicePool

* feat: update CertifyMissionRequest

* feat: add custom Json instance

* feat: implement CertifyMissionResponse toEntity()

* chore: update HomeActions

* chore: update PickMedia callback

* chore: add new HomeDialogState

* chore: update actions

* feat: add MissionCertificationResultDialog

* feat: add MissionRepository

* feat: add ContextExtension

* feat: add MissionCertificationType

* feat: add MissionCertificationInfo

* feat: implement CertifyMissionUseCase

* feat: add new state for HomeUiState.SUCCESS

* chore: rename

* chore: update certifyMission

* chore: implement certifyMission logic
  • Loading branch information
giovannijunseokim authored Apr 1, 2024
1 parent e3aba7b commit 71980cd
Show file tree
Hide file tree
Showing 21 changed files with 337 additions and 61 deletions.
21 changes: 15 additions & 6 deletions app/src/main/java/univ/earthbreaker/namu/data/ApiFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import univ.earthbreaker.namu.BuildConfig
import univ.earthbreaker.namu.data.service.OpenAIApiService

object ApiFactory {
private val json: Json by lazy {
Json {
encodeDefaults = true
}
}

private val client: OkHttpClient by lazy {
OkHttpClient.Builder().addInterceptor(
HttpLoggingInterceptor().apply {
Expand All @@ -21,25 +28,27 @@ object ApiFactory {
val retrofitForGrowTreeServer: Retrofit by lazy {
Retrofit.Builder().baseUrl(BuildConfig.GROW_TREE_BASE_URL).client(client)
.addConverterFactory(
Json.asConverterFactory("application/json".toMediaType()),
json.asConverterFactory("application/json".toMediaType()),
).build()
}

val retrofitForOpenAiService: Retrofit by lazy {
Retrofit.Builder().baseUrl(BuildConfig.OPEN_AI_BASE_URL).client(client)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType())).build()
Retrofit.Builder().baseUrl(BuildConfig.OPENAI_BASE_URL).client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())).build()
}

inline fun <reified T> create(forWhichServer: Server): T =
when (forWhichServer) {
Server.GrowTreeServer -> retrofitForGrowTreeServer.create(T::class.java)
Server.ChatGPT -> retrofitForOpenAiService.create(T::class.java)
Server.OpenAI -> retrofitForOpenAiService.create(T::class.java)
}
}

enum class Server {
GrowTreeServer,
ChatGPT,
OpenAI,
}

object ServicePool
object ServicePool {
val openAIApiService: OpenAIApiService = ApiFactory.create(forWhichServer = Server.OpenAI)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package univ.earthbreaker.namu.data.model.home

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CertifyMissionRequest(
val model: String = "gpt-4-vision-preview",
val messages: List<Message>,
@SerialName("max_tokens") val maxTokens: Int = 300,
) {
@Serializable
data class Message(
val role: String = "user",
@SerialName("content") val contents: List<ImageContent>,
) {
@Serializable
data class ImageContent(
@SerialName("image_url") val imageUrl: ImageUrl,
val prompt: String,
@SerialName("type") val contentType: String = "image_url",
) {
@Serializable
data class ImageUrl(
val url: String,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package univ.earthbreaker.namu.data.model.home

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo

@Serializable
data class CertifyMissionResponse(
val id: String,
@SerialName("object") val objectType: String = "chat.completion",
val created: Int,
val model: String,
@SerialName("system_fingerprint") val systemFingerprint: String,
val choices: List<Choice>,
val usage: Usage,
) {
@Serializable
data class Choice(
val index: Int,
val message: Message,
@SerialName("finish_reason") val finishReason: String,
val logprobs: Content?,
) {
@Serializable
data class Message(
val role: String,
val content: String?,
)

@Serializable
data class Content(
val token: String,
val logprob: Int,
val bytes: List<Int>?,
val top_logprobs: List<LogProb>,
) {
@Serializable
data class LogProb(
val token: String,
val logprob: Int,
val bytes: List<Int>?,
)
}
}

@Serializable
data class Usage(
@SerialName("completion_tokens") val completionTokens: Int,
@SerialName("prompt_tokens") val promptTokens: Int,
@SerialName("total_tokens") val totalTokens: Int,
)

fun toEntity(): MissionCertificationInfo =
MissionCertificationInfo(isSuccessful = this.choices.first().message.content.toBoolean())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package univ.earthbreaker.namu.data.repository.mission

import univ.earthbreaker.namu.data.ServicePool
import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest
import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest.Message
import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest.Message.ImageContent
import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest.Message.ImageContent.ImageUrl
import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo
import univ.earthbreaker.namu.domain.repository.MissionRepository

class MissionRepositoryImpl : MissionRepository {
override suspend fun certifyMission(
prompt: String,
imageBase64: String,
): MissionCertificationInfo =
ServicePool.openAIApiService.certifyMission(
certifyMissionRequest = CertifyMissionRequest(
messages = listOf(
Message(
contents = listOf(
ImageContent(
imageUrl = ImageUrl(
url = imageBase64,
),
prompt = prompt,
),
),
),
),
),
).toEntity()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package univ.earthbreaker.namu.data.service

import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
import univ.earthbreaker.namu.BuildConfig
import univ.earthbreaker.namu.data.model.home.CertifyMissionRequest
import univ.earthbreaker.namu.data.model.home.CertifyMissionResponse

interface OpenAIApiService {
@POST("chat/completions")
suspend fun certifyMission(
@Header("Authorization") token: String = "Bearer ${BuildConfig.OPENAI_API_KEY}",
@Body certifyMissionRequest: CertifyMissionRequest,
): CertifyMissionResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package univ.earthbreaker.namu.domain.entity.mission

data class MissionCertificationInfo(val isSuccessful: Boolean)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package univ.earthbreaker.namu.domain.entity.mission

data class MissionItem(
data class MissionInfo(
val missionName: String,
val beforeChallenge: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package univ.earthbreaker.namu.domain.repository

import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo

interface MissionRepository {
suspend fun certifyMission(
prompt: String,
imageBase64: String,
): MissionCertificationInfo
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package univ.earthbreaker.namu.domain.usecase.mission

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import univ.earthbreaker.namu.data.repository.mission.MissionRepositoryImpl
import univ.earthbreaker.namu.domain.entity.mission.MissionCertificationInfo
import univ.earthbreaker.namu.domain.repository.MissionRepository

class CertifyMissionUseCase(private val missionRepository: MissionRepository = MissionRepositoryImpl()) {
suspend operator fun invoke(imageBase64: String): Flow<MissionCertificationInfo> =
flow {
emit(
missionRepository.certifyMission(
imageBase64 = imageBase64,
prompt = """you're a helpful assistant who classify image by true and false.
if an image is related to practicing environmental conservation,
answer true. else false.
""".trimIndent(),
),
)
}.flowOn(
Dispatchers.IO,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.activity.viewModels
import univ.earthbreaker.namu.ui.theme.GTTheme
import univ.earthbreaker.namu.util.getBase64FromUri

class HomeActivity : ComponentActivity() {
private val viewModel: HomeViewModel by viewModels()
private val pickMedia = registerForActivityResult(PickVisualMedia()) { uri ->
viewModel.setMissionCertificationUri(imageUri = uri)
viewModel.setMissionImage(imageUri = uri, base64 = getBase64FromUri(uri = uri))
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
18 changes: 5 additions & 13 deletions app/src/main/java/univ/earthbreaker/namu/feature/home/HomeRoute.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,11 @@ fun rememberHomeActions(viewModel: HomeViewModel, onClickCameraIcon: () -> Unit)
return remember(viewModel) {
HomeActions(
onClickCameraIcon = onClickCameraIcon,
openDialog = { homeDialogState -> viewModel.openDialog(homeDialogState = homeDialogState) },
dismissDialog = { viewModel.dismissDialog() },
levelUpCharacter = { viewModel.levelUpCharacter() },
onMissionDescriptionChanged = { newMissionDescription ->
viewModel.onMissionDescriptionChanged(
newMissionDescription = newMissionDescription,
)
},
onPickImage = { uri -> viewModel.setMissionCertificationUri(uri) },
missionCertificationActions = HomeActions.MissionCertificationActions(
certifyMission = { viewModel.certifyMission() },
uploadMission = { viewModel.uploadMission() },
),
openDialog = viewModel::openDialog,
dismissDialog = viewModel::dismissDialog,
levelUpCharacter = viewModel::levelUpCharacter,
onMissionDescriptionChanged = viewModel::onMissionDescriptionChanged,
certifyMission = viewModel::certifyMission,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import univ.earthbreaker.namu.feature.home.model.HomeDialogState
import univ.earthbreaker.namu.feature.home.model.HomeUiState
import univ.earthbreaker.namu.feature.home.type.CharacterLevel
import univ.earthbreaker.namu.feature.mission.MissionCertificationDialog
import univ.earthbreaker.namu.feature.mission.MissionCertificationResultDialog
import univ.earthbreaker.namu.feature.mission.MissionListDialog
import univ.earthbreaker.namu.ui.theme.GTTheme
import univ.earthbreaker.namu.ui.theme.navigationBarHeight
Expand Down Expand Up @@ -118,10 +119,17 @@ fun HomeScreen(
},
missionName = "비건 카페 방문하기",
missionDescription = state.missionDescription,
missionImageUri = state.missionCertificationUri,
missionImageUri = state.missionImageUri,
onClickCameraImage = actions.onClickCameraIcon,
onMissionDescriptionChanged = actions.onMissionDescriptionChanged,
uploadMission = actions.missionCertificationActions.uploadMission,
certifyMission = actions.certifyMission,
)
}

if (state.homeDialogState.doesMissionCertificationResultDialogExist) {
MissionCertificationResultDialog(
isSuccessful = state.isMissionCertificationSuccessful ?: false,
onDismissRequest = {},
)
}
}
Expand Down Expand Up @@ -150,7 +158,7 @@ fun HomeTopCard(
.padding(start = 7.dp)
.height(11.dp)
.clip(RoundedCornerShape(20.dp)),
progress = userExp,
progress = { userExp },
color = GTTheme.colors.green1,
trackColor = GTTheme.colors.bg2Black,
)
Expand Down Expand Up @@ -236,7 +244,7 @@ private fun HomeScreenPreview() {
HomeScreen(
state = HomeUiState.Success(
HomeInfo(),
missionCertificationUri = null,
missionImageUri = null,
missionDescription = "",
),
actions = HomeActions(
Expand All @@ -245,8 +253,7 @@ private fun HomeScreenPreview() {
dismissDialog = {},
levelUpCharacter = {},
onMissionDescriptionChanged = {},
onPickImage = {},
HomeActions.MissionCertificationActions(certifyMission = {}, uploadMission = {}),
certifyMission = {},
),
)
}
Expand Down
Loading

0 comments on commit 71980cd

Please sign in to comment.