Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement open ai service #13

Merged
merged 23 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6b31059
feat: implement CertifyMissionRequest
giovannijunseokim Apr 1, 2024
b64985e
feat: implement CertifyMissionResponse
giovannijunseokim Apr 1, 2024
7664235
chore: change local.properties keys
giovannijunseokim Apr 1, 2024
df431e7
feat: implement OpenAiApiService
giovannijunseokim Apr 1, 2024
0889b55
chore: rename OpenAi to OpenAI
giovannijunseokim Apr 1, 2024
4e443ce
feat: add openAIApiService into ServicePool
giovannijunseokim Apr 1, 2024
1bf5876
feat: update CertifyMissionRequest
giovannijunseokim Apr 1, 2024
c504998
feat: add custom Json instance
giovannijunseokim Apr 1, 2024
f3c6f79
feat: implement CertifyMissionResponse toEntity()
giovannijunseokim Apr 1, 2024
b719b87
chore: update HomeActions
giovannijunseokim Apr 1, 2024
9c3309f
chore: update PickMedia callback
giovannijunseokim Apr 1, 2024
6c65e8d
chore: add new HomeDialogState
giovannijunseokim Apr 1, 2024
84418c7
chore: update actions
giovannijunseokim Apr 1, 2024
48bd225
feat: add MissionCertificationResultDialog
giovannijunseokim Apr 1, 2024
7019acb
feat: add MissionRepository
giovannijunseokim Apr 1, 2024
47a9628
feat: add ContextExtension
giovannijunseokim Apr 1, 2024
f3b2d89
feat: add MissionCertificationType
giovannijunseokim Apr 1, 2024
1650b37
feat: add MissionCertificationInfo
giovannijunseokim Apr 1, 2024
81d618d
feat: implement CertifyMissionUseCase
giovannijunseokim Apr 1, 2024
8122439
feat: add new state for HomeUiState.SUCCESS
giovannijunseokim Apr 1, 2024
deb3bb9
chore: rename
giovannijunseokim Apr 1, 2024
22ac898
chore: update certifyMission
giovannijunseokim Apr 1, 2024
541394d
chore: implement certifyMission logic
giovannijunseokim Apr 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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