Skip to content

Commit

Permalink
feat: iOS sample integration
Browse files Browse the repository at this point in the history
  • Loading branch information
renatoarg committed Mar 14, 2023
1 parent 9310e86 commit 755c93f
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() {

private val imageGenerations by lazy {
chatGpt.imageGenerations()
.setResults(2)
}

private val _items = mutableStateListOf<MessageItem>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ fun SendMessageLayout() {
val viewModel = koinViewModel<MainViewModel>()
val isLoading: Boolean by viewModel.isLoading.observeAsState(initial = false)


Row(
modifier = Modifier
.background(color = MaterialTheme.colors.background)
Expand Down
33 changes: 30 additions & 3 deletions sample/ios/YChatApp/Features/Completion/CompletionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,14 @@ private extension CompletionView {
}
case .bot:
HStack {
botChatBubble(message: chatMessage.message)
Spacer().frame(width: 60)
Spacer()
if let imageUrl = chatMessage.url {
botImageBubble(imageUrl)
Spacer()
} else {
botChatBubble(message: chatMessage.message)
Spacer().frame(width: 60)
Spacer()
}
}
case .loading:
HStack {
Expand Down Expand Up @@ -120,6 +125,28 @@ private extension CompletionView {
.cornerRadius(16, corners: [.bottomLeft, .bottomLeft, .topRight])
}
}

@ViewBuilder
private func botImageBubble(_ url: String) -> some View {
HStack(alignment: .top, spacing: 4) {
Circle()
.fill(.green)
.frame(width: 40, height: 40)
.overlay {
Image(uiImage: Icon.bot.uiImage)
.renderingMode(.template)
.foregroundColor(.white)
}
ZStack {
AsyncImage(url: URL(string: url))
.foregroundColor(.grayDark)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.grayLight)
.cornerRadius(16, corners: [.bottomLeft, .bottomLeft, .topRight])
}
}

@ViewBuilder
private func sendMessageSection() -> some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct ChatMessage: Identifiable, Equatable {
let id: String
var message: String = ""
var type: MessageType = .human(error: false)
var url: String?

enum MessageType: Equatable {
case human(error: Bool), bot, loading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ internal final class CompletionViewModel: ObservableObject {
content: "You are a helpful assistant."
)

private var imageGenerations: ImageGenerations =
YChatCompanion.shared.create(apiKey: Config.apiKey)
.imageGenerations()

@Published
var message: String = ""

Expand All @@ -37,9 +41,15 @@ internal final class CompletionViewModel: ObservableObject {
cleanLastMessage()
addLoading()
do {
let result = try await chatCompletions.execute(content: input)[0].content
removeLoading()
addAIMessage(message: result)
if input.contains("/image ") {
let result = try await imageGenerations.execute(prompt: input)[0].url
removeLoading()
addAIImage(url: result)
} else {
let result = try await chatCompletions.execute(content: input)[0].content
removeLoading()
addAIMessage(message: result)
}
} catch {
removeLoading()
setError()
Expand All @@ -64,6 +74,15 @@ internal final class CompletionViewModel: ObservableObject {
)
chatMessageList.append(chatMessage)
}

private func addAIImage(url: String) {
let chatMessage = ChatMessage(
id: UUID().uuidString,
type: .bot,
url: url
)
chatMessageList.append(chatMessage)
}

private func addLoading() {
let chatMessage = ChatMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public ResponseEntity<String> chatCompletions(
return ResponseEntity.ok(result);
}

@GetMapping("generations")
public ResponseEntity<String> imageGenerations(
@RequestParam(value = "prompt", defaultValue = Defaults.CHAT_COMPLETION_INPUT) String input
) throws Exception {
String result = YChatService.getImageGenerationsAnswer(input);
return ResponseEntity.ok(result);
}

private static class Defaults {
static final String COMPLETION_INPUT = "Say this is a test.";
static final String CHAT_COMPLETION_INPUT = "Tell me one strength exercise";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import co.yml.ychat.YChat;
import co.yml.ychat.domain.model.ImageGenerated;

@Service
public class YChatService {
Expand Down Expand Up @@ -35,6 +36,13 @@ public String getChatCompletionsAnswer(String input, String topic) throws Except
return future.get().get(0).getContent();
}

public String getImageGenerationsAnswer(String prompt) throws Exception {
final CompletableFuture<List<String>> future = new CompletableFuture<>();
ychat.imageGenerations()
.execute(prompt, new CompletionCallbackResult<>(future));
return future.get().get(0);
}

private static class CompletionCallbackResult<T> implements YChat.Callback<T> {

private final CompletableFuture<T> future;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal fun ImageGenerationsDto.toImageGenerated(): List<ImageGenerated> {
}
}


internal fun ImageGenerationsParams.toImageGenerationsParamsDto(): ImageGenerationsParamsDto {
return ImageGenerationsParamsDto(
prompt = this.prompt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Represents a message in a conversation, consisting of a [role] indicating the speaker
* (e.g., “system”, “user” or “assistant”), and the [content] of the message sent by the speaker.
* @property role The role of the speaker who sends the message.
* @property content The content of the message sent by the speaker.
* Represents a image generated by a prompt, consisting of a [url] generated by the system (AI).
* @property url The url of the image generated by the input provided.
*/
@Serializable
data class ImageGenerated(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
package co.yml.ychat.domain.model

/**
* Parameters to configure the Image Generations API.
*
* @param prompt The prompt(s) to generate images for.
*
* @param results: Quantity of images to be generated.
*
* @param size: The size of the images generated (squared). Ex. 256x256, 512x512, 1024x1024
*
* @param responseFormat: The format in which the generated images are returned. Must be one of url or b64_json.
*
* @param user: A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
*/
internal data class ImageGenerationsParams(
var prompt: String = "",
var results: Int = 2,
var results: Int = 1,
var size: String = "256x256",
var responseFormat: String = "url",
var user: String = "",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package co.yml.ychat.entrypoint.impl

import co.yml.ychat.YChat
import co.yml.ychat.domain.model.ChatCompletionsParams
import co.yml.ychat.domain.model.ImageGenerated
import co.yml.ychat.domain.model.ImageGenerationsParams
import co.yml.ychat.domain.usecases.ImageGenerationsUseCase
Expand Down Expand Up @@ -43,10 +42,8 @@ internal class ImageGenerationsImpl(
override fun execute(prompt: String, callback: YChat.Callback<List<String>>) {
scope.launch {
kotlin.runCatching { execute(prompt) }
.onSuccess { it -> callback.onSuccess(it.map { it.url }) } // fix here
.onSuccess { callback.onSuccess(it.map { it.url }) }
.onFailure { callback.onError(it) }
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import co.yml.ychat.data.storage.ChatLogStorage
import co.yml.ychat.di.module.LibraryModule
import co.yml.ychat.domain.usecases.ChatCompletionsUseCase
import co.yml.ychat.domain.usecases.CompletionUseCase
import co.yml.ychat.domain.usecases.ImageGenerationsUseCase
import co.yml.ychat.entrypoint.features.ChatCompletions
import co.yml.ychat.entrypoint.features.Completion
import co.yml.ychat.entrypoint.features.ImageGenerations
import io.ktor.client.HttpClient
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
Expand Down Expand Up @@ -39,5 +41,7 @@ class LibraryModuleTest : KoinTest {
get<CompletionUseCase>()
get<ChatCompletionsUseCase>()
get<ChatCompletions>()
get<ImageGenerationsUseCase>()
get<ImageGenerations>()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package co.yml.ychat.domain.mapper

import co.yml.ychat.data.dto.ChatCompletionsChoiceDto
import co.yml.ychat.data.dto.ChatCompletionsDto
import co.yml.ychat.data.dto.ChatMessageDto
import co.yml.ychat.data.dto.UsageDto
import co.yml.ychat.domain.model.ChatCompletionsParams
import co.yml.ychat.domain.model.ChatMessage
import kotlin.test.Test
import kotlin.test.assertEquals

class ChatCompletionsMapperTest {

@Test
fun `on convert ChatCompletionsDto to ChatMessages`() {
val listOfChatMessages = listOf(ChatMessage("user", "message 1"), ChatMessage("user", "message 2"))
val chatCompletionsDto = ChatCompletionsDto(
choices = listOf(
ChatCompletionsChoiceDto(1, ChatMessageDto("user", "message 1"), null),
ChatCompletionsChoiceDto(1, ChatMessageDto("user", "message 2"), null)
),
id = "1",
model = "",
usage = UsageDto(1, 1, 1)
)
assertEquals(listOfChatMessages, chatCompletionsDto.toChatMessages())
}


@Test
fun `on convert ChatCompletionsParams to ChatCompletionParamsDto`() {
val messages = arrayListOf(ChatMessage("user", "message 1"), ChatMessage("user", "message 2"))
val chatCompletionsParams = ChatCompletionsParams(messages)
assertEquals(messages.map { ChatMessageDto(it.role, it.content) }, chatCompletionsParams.toChatCompletionParamsDto().messages, "")
assertEquals("gpt-3.5-turbo", chatCompletionsParams.toChatCompletionParamsDto().model)
assertEquals(1, chatCompletionsParams.toChatCompletionParamsDto().maxResults)
assertEquals(4096, chatCompletionsParams.toChatCompletionParamsDto().maxTokens)
assertEquals(1.0, chatCompletionsParams.toChatCompletionParamsDto().temperature)
assertEquals(1.0, chatCompletionsParams.toChatCompletionParamsDto().topP)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package co.yml.ychat.domain.mapper

import co.yml.ychat.data.dto.ImageGenerationsDto
import co.yml.ychat.domain.model.ImageGenerated
import co.yml.ychat.domain.model.ImageGenerationsParams
import kotlin.test.Test
import kotlin.test.assertEquals

class ImageGenerationsMapperTest {

@Test
fun `on convert ImageGenerationsDto to ImageGenerated`() {
val listOfImageGenerated = listOf(ImageGenerated("http://url1.test"), ImageGenerated("http://url2.test"))
val imageGenerationsDto = ImageGenerationsDto(
created = 12345,
data = listOfImageGenerated
)
assertEquals(listOfImageGenerated, imageGenerationsDto.toImageGenerated())
}

@Test
fun `on convert ImageGenerationsParams to ImageGenerationsDto`() {
val imageGenerationsParams = ImageGenerationsParams(prompt = "/image test")
assertEquals("/image test", imageGenerationsParams.toImageGenerationsParamsDto().prompt)
assertEquals("url", imageGenerationsParams.toImageGenerationsParamsDto().responseFormat)
assertEquals("256x256", imageGenerationsParams.toImageGenerationsParamsDto().size)
assertEquals("", imageGenerationsParams.toImageGenerationsParamsDto().user)
assertEquals(1, imageGenerationsParams.toImageGenerationsParamsDto().results)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package co.yml.ychat.domain.model

import kotlin.test.Test
import kotlin.test.assertEquals

class ImageGenerationsParamsTest {

@Test
fun `on ChatCompletionsParams verify default values`() {
// arrange
val params = ImageGenerationsParams()

// assert
assertEquals(true, params.prompt.isEmpty())
assertEquals(1, params.results)
assertEquals("256x256", params.size)
assertEquals("url", params.responseFormat)
assertEquals("", params.user)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package co.yml.ychat.domain.usecases

import co.yml.ychat.data.api.ChatGptApi
import co.yml.ychat.data.dto.ImageGenerationsDto
import co.yml.ychat.data.exception.ChatGptException
import co.yml.ychat.data.infrastructure.ApiResult
import co.yml.ychat.domain.model.ImageGenerated
import co.yml.ychat.domain.model.ImageGenerationsParams
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

class ImageGenerationsUseCaseTest {

private lateinit var imageGenerationsUseCase: ImageGenerationsUseCase

private val chatGptApiMock = mockk<ChatGptApi>()

@BeforeTest
fun setup() {
imageGenerationsUseCase = ImageGenerationsUseCase(chatGptApiMock)
}

@Test
fun `on requestImageGenerations when request succeed then should return formatted result`() {
// arrange
val prompt = "/image test"
val imageGenerationsDto = buildImageGenerationsDto("https://image-generated.test")
val params = ImageGenerationsParams(prompt = prompt)
val apiResult = ApiResult(body = imageGenerationsDto)
coEvery { chatGptApiMock.imageGenerations(any()) } returns apiResult

// act
val result = runBlocking { imageGenerationsUseCase.requestImageGenerations(params) }

// assert
assertEquals("https://image-generated.test", result.last().url)
}

@Test
fun `on requestChatCompletions when not request succeed then should throw an exception`() {
// arrange
val prompt = "/image test"
val params = ImageGenerationsParams(prompt = prompt)
val apiResult = ApiResult<ImageGenerationsDto>(exception = ChatGptException())
coEvery { chatGptApiMock.imageGenerations(any()) } returns apiResult

// act
val result =
runCatching { runBlocking { imageGenerationsUseCase.requestImageGenerations(params) } }

// assert
assertEquals(true, result.exceptionOrNull() is ChatGptException)
}

private fun buildImageGenerationsDto(url: String): ImageGenerationsDto {
return ImageGenerationsDto(
created = 12345,
data = listOf(ImageGenerated(url))
)
}
}

0 comments on commit 755c93f

Please sign in to comment.