Skip to content

Commit

Permalink
test(anthropic-client): add tests for message serialization
Browse files Browse the repository at this point in the history
Enhanced testing for Message and Content serialization in the anthropic-client module, including support for multiple content types like text and image. Added necessary dependencies to build configurations.
  • Loading branch information
hanrw committed Jan 13, 2025
1 parent b220f50 commit 7ad5576
Show file tree
Hide file tree
Showing 9 changed files with 488 additions and 8 deletions.
1 change: 1 addition & 0 deletions anthropic-client/anthropic-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ kotlin {
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("org.reflections:reflections:0.10.2")
implementation(libs.org.skyscreamer.jsonassert)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.tddworks.anthropic.api.messages.api

import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*

object ContentSerializer : KSerializer<Content> {
override val descriptor: SerialDescriptor
get() = buildClassSerialDescriptor("Content")

override fun deserialize(decoder: Decoder): Content {
val jsonElement = decoder.decodeSerializableValue(JsonElement.serializer())
return when (jsonElement) {
is JsonPrimitive -> Content.TextContent(jsonElement.content)
is JsonArray -> {
val items = jsonElement.map { element ->
val jsonObj = element.jsonObject

when (jsonObj["type"]?.jsonPrimitive?.content) {
"text" -> BlockMessageContent.TextContent(
text = jsonObj["text"]?.jsonPrimitive?.content
?: throw IllegalArgumentException("Missing text")
)

"image" -> BlockMessageContent.ImageContent(
source = BlockMessageContent.ImageContent.Source(
mediaType = jsonObj["source"]?.jsonObject?.get("media_type")?.jsonPrimitive?.content
?: throw IllegalArgumentException("Missing media_type"),
data = jsonObj["source"]?.jsonObject?.get("data")?.jsonPrimitive?.content
?: throw IllegalArgumentException("Missing data"),
type = jsonObj["source"]?.jsonObject?.get("type")?.jsonPrimitive?.content
?: throw IllegalArgumentException("Missing type")
)
)

else -> throw IllegalArgumentException("Unsupported content block type")
}

}
Content.BlockContent(blocks = items)
}

else -> throw IllegalArgumentException("Unsupported content format")
}
}

override fun serialize(encoder: Encoder, value: Content) {
when (value) {
is Content.TextContent -> encoder.encodeString(value.text)
is Content.BlockContent -> encoder.encodeSerializableValue(
ListSerializer(BlockMessageContent.serializer()), value.blocks
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tddworks.anthropic.api.messages.api

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

/**
Expand All @@ -13,13 +14,73 @@ import kotlinx.serialization.Serializable
* Example with a single user message:
*
* [{"role": "user", "content": "Hello, Claude"}]
*
* Each input message content may be either a single string or an array of content blocks, where each block has a specific type. Using a string for content is shorthand for an array of one content block of type "text". The following input messages are equivalent:
* {"role": "user", "content": "Hello, Claude"}
* {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]}
*/
@Serializable
data class Message(
val role: Role,
val content: String,
val content: Content,
) {
companion object {
fun user(content: String) = Message(Role.User, content)
fun user(content: String) = Message(Role.User, Content.TextContent(content))
}
}

@Serializable(with = ContentSerializer::class)
sealed interface Content {
data class TextContent(
val text: String,
) : Content

data class BlockContent(
val blocks: List<BlockMessageContent>,
) : Content
}


/**
* https://docs.anthropic.com/en/docs/build-with-claude/vision#prompt-examples
* {
* "role": "user",
* "content": [
* {
* "type": "image",
* "source": {
* "type": "base64",
* "media_type": image1_media_type,
* "data": image1_data,
* },
* },
* {
* "type": "text",
* "text": "Describe this image."
* }
* ],
* }
*/
@Serializable
sealed interface BlockMessageContent {

@Serializable
@SerialName("image")
data class ImageContent(
val source: Source
) : BlockMessageContent {
@Serializable
data class Source(
@SerialName("media_type") val mediaType: String,
val data: String,
val type: String
)
}
}

@Serializable
@SerialName("text")
data class TextContent(
val text: String,
) : BlockMessageContent

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.tddworks.anthropic.api.messages.api

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject

/**
* {
* "role": "user",
* "content": [
* {
* "type": "image",
* "source": {
* "type": "base64",
* "media_type": image1_media_type,
* "data": image1_data,
* },
* },
* {
* "type": "text",
* "text": "Describe this image."
* }
* ],
* }
*/
//internal object MessageContentSerializer :
// JsonContentPolymorphicSerializer<Content>(Content::class) {
// override fun selectDeserializer(element: JsonElement): KSerializer<out Content> {
// val jsonObject = element.jsonObject
// return when {
// "source" in jsonObject -> BlockMessageContent.ImageContent.serializer()
// "text" in jsonObject -> BlockMessageContent.TextContent.serializer()
// else -> throw SerializationException("Unknown Content type")
// }
//
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.tddworks.anthropic.api.messages.api

import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import org.skyscreamer.jsonassert.JSONAssert

class BlockMessageContentTest {

@Test
fun `should serialize image message content`() {
// Given
val messageContent = BlockMessageContent.ImageContent(
source = BlockMessageContent.ImageContent.Source(
mediaType = "image1_media_type",
data = "image1_data",
type = "base64",
),
)

// When
val result = Json.encodeToString(
BlockMessageContent.serializer(),
messageContent
)

// Then
JSONAssert.assertEquals(
"""
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image1_media_type",
"data": "image1_data"
}
}
""".trimIndent(),
result,
false
)
}

@Test
fun `should serialize message content`() {
// Given
val messageContent = BlockMessageContent.TextContent(
text = "some-text",
)

// When
val result = Json.encodeToString(
BlockMessageContent.serializer(),
messageContent
)

// Then
JSONAssert.assertEquals(
"""
{
"text": "some-text",
"type": "text"
}
""".trimIndent(),
result,
false
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.tddworks.anthropic.api.messages.api

import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.skyscreamer.jsonassert.JSONAssert


/**
* Each input message content may be either a single string or an array of content blocks, where each block has a specific type. Using a string for content is shorthand for an array of one content block of type "text". The following input messages are equivalent:
* {"role": "user", "content": "Hello, Claude"}
* {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]}
*/
class ContentTest {

@Test
fun `should serialize multiple content`() {
// Given
val content = Content.BlockContent(
listOf(
BlockMessageContent.ImageContent(
source = BlockMessageContent.ImageContent.Source(
mediaType = "image1_media_type",
data = "image1_data",
type = "base64",
),
),
BlockMessageContent.TextContent(
text = "some-text",
),
)
)

// When
val result = Json.encodeToString(
Content.serializer(),
content
)

// Then
JSONAssert.assertEquals(
"""
[
{
"source": {
"data": "image1_data",
"media_type": "image1_media_type",
"type": "base64"
},
"type": "image"
},
{
"text": "some-text",
"type": "text"
}
]
""".trimIndent(),
result,
false
)
}

@Test
fun `should serialize single string content`() {
// Given
val content = Content.TextContent("Hello, Claude")

// When
val result = Json.encodeToString(
Content.serializer(),
content
)

// Then
assertEquals(
"""
"Hello, Claude"
""".trimIndent(), result
)
}

}
Loading

0 comments on commit 7ad5576

Please sign in to comment.