Skip to content

Commit

Permalink
feat(BE-174): integrate anthropic api
Browse files Browse the repository at this point in the history
 - add koin-core support
 - add generic ChatApi and unit test
  • Loading branch information
hanrw committed Mar 15, 2024
1 parent 82c1033 commit 85934b8
Show file tree
Hide file tree
Showing 25 changed files with 428 additions and 94 deletions.
4 changes: 4 additions & 0 deletions anthropic-client/anthropic-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
// put your Multiplatform dependencies here
implementation(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.json)
api(libs.bundles.ktor.client)
api(projects.common)
Expand All @@ -33,6 +34,9 @@ kotlin {
jvmTest.dependencies {
implementation(project.dependencies.platform(libs.junit.bom))
implementation(libs.bundles.jvm.test)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("org.reflections:reflections:0.10.2")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package com.tddworks.anthropic.api.messages.api

import com.tddworks.common.network.api.StreamableRequest
import com.tddworks.openllm.api.ChatRequest
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("CreateMessageRequest")
data class CreateMessageRequest(
val messages: List<Message>,
val systemPrompt: String? = null,
) : ChatRequest
) : ChatRequest, StreamableRequest {
companion object {
fun streamRequest(messages: List<Message>, systemPrompt: String? = null) =
CreateMessageRequest(messages, systemPrompt) as StreamableRequest
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ data class CreateMessageResponse(
val model: String,
val role: String,
@SerialName("stop_reason")
val stopReason: String,
val stopReason: String?,
@SerialName("stop_sequence")
val stopSequence: String?,
val type: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
package com.tddworks.anthropic.api.messages.api.internal

import com.tddworks.common.network.api.StreamChatResponse
import com.tddworks.common.network.api.StreamableRequest
import com.tddworks.common.network.api.ktor.api.HttpRequester
import com.tddworks.common.network.api.ktor.api.performRequest
import com.tddworks.common.network.api.ktor.api.streamRequest
import com.tddworks.openllm.api.ChatApi
import com.tddworks.openllm.api.ChatRequest
import com.tddworks.openllm.api.ChatResponse
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.json.Json

/**
* * Anthropic Messages API -https://docs.anthropic.com/claude/reference/messages_post
*/
class DefaultMessagesApi(private val requester: HttpRequester) : ChatApi {
class DefaultMessagesApi(
private val requester: HttpRequester,
private val jsonLenient: Json = JsonLenient,
) : ChatApi {

override fun chat(request: StreamableRequest): Flow<StreamChatResponse> {
return requester.streamRequest<StreamChatResponse> {
method = HttpMethod.Post
url(path = CHAT_COMPLETIONS_PATH)
setBody(request.asStreamRequest(jsonLenient))
contentType(ContentType.Application.Json)
accept(ContentType.Text.EventStream)
headers {
append(HttpHeaders.CacheControl, "no-cache")
append(HttpHeaders.Connection, "keep-alive")
}
}
}

/**
* Create a message.
* @param request Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tddworks.anthropic.api.messages.api.internal

import com.tddworks.anthropic.api.messages.api.internal.json.chatModule
import kotlinx.serialization.json.Json


/**
* Represents a JSON object that allows for leniency and ignores unknown keys.
*
* @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string literals are allowed.
* Its relaxations can be expanded in the future, so that lenient parser becomes even more permissive to invalid value in the input, replacing them with defaults.
* false by default.
* @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON should be ignored instead of throwing SerializationException. false by default..
*/
val JsonLenient = Json {
isLenient = true
ignoreUnknownKeys = true
// https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#class-discriminator-for-polymorphism
classDiscriminator = "#class"
serializersModule = chatModule
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.tddworks.anthropic.api.messages.api.internal.json

import com.tddworks.anthropic.api.messages.api.CreateMessageRequest
import com.tddworks.anthropic.api.messages.api.CreateMessageResponse
import com.tddworks.anthropic.api.messages.api.stream.*
import com.tddworks.common.network.api.StreamChatResponse
import com.tddworks.common.network.api.StreamableRequest
import com.tddworks.openllm.api.ChatRequest
import com.tddworks.openllm.api.ChatResponse
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic

val chatModule = SerializersModule {

// polymorphicDefaultSerializer(StreamableRequest::class) { instance ->
// @Suppress("UNCHECKED_CAST")
// when (instance) {
// is CreateMessageRequest -> CreateMessageRequest.serializer() as SerializationStrategy<StreamableRequest>
// else -> null
// }
// }

polymorphic(StreamableRequest::class) {
subclass(CreateMessageRequest::class, CreateMessageRequest.serializer())
defaultDeserializer { CreateMessageRequest.serializer() }
}
//
polymorphic(ChatRequest::class) {
subclass(CreateMessageRequest::class, CreateMessageRequest.serializer())
defaultDeserializer { CreateMessageRequest.serializer() }
}
//
// polymorphic(ChatResponse::class) {
// subclass(CreateMessageResponse::class, CreateMessageResponse.serializer())
// defaultDeserializer { ChatResponseSerializer }
// }

polymorphic(StreamChatResponse::class) {
defaultDeserializer { StreamChatResponseSerializer }
}

// polymorphicDefaultSerializer(ChatResponse::class) { instance ->
// @Suppress("UNCHECKED_CAST")
// when (instance) {
// is CreateMessageResponse -> CreateMessageResponse.serializer() as SerializationStrategy<ChatResponse>
// else -> null
// }
// }
//
polymorphic(ChatResponse::class) {
defaultDeserializer { ChatResponseSerializer }
}
}

object ChatResponseSerializer :
JsonContentPolymorphicSerializer<ChatResponse>(ChatResponse::class) {
override fun selectDeserializer(element: JsonElement): KSerializer<out ChatResponse> {
val content = element.jsonObject["content"]

return when {
content != null -> CreateMessageResponse.serializer()
else -> throw IllegalArgumentException("Unknown type of message")
}
}
}

object StreamChatResponseSerializer :
JsonContentPolymorphicSerializer<StreamChatResponse>(StreamChatResponse::class) {
override fun selectDeserializer(element: JsonElement): KSerializer<out StreamChatResponse> {
val type = element.jsonObject["type"]?.jsonPrimitive?.content

return when (type) {
"message_start" -> MessageStart.serializer()
"content_block_start" -> ContentBlockStart.serializer()
"content_block_delta" -> ContentBlockDelta.serializer()
"content_block_stop" -> ContentBlockStop.serializer()
"message_delta" -> MessageDelta.serializer()
"message_stop" -> MessageStop.serializer()
"ping" -> Ping.serializer()
else -> throw IllegalArgumentException("Unknown type of message")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.tddworks.anthropic.api.messages.api.stream

import com.tddworks.anthropic.api.messages.api.CreateMessageResponse
import com.tddworks.common.network.api.StreamChatResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@SerialName("message_start")
@Serializable
data class MessageStart(
override val type: String,
val message: CreateMessageResponse,
) : StreamChatResponse

@Serializable
@SerialName("content_block_start")
data class ContentBlockStart(
override val type: String,
val index: Int,
@SerialName("content_block")
val contentBlock: ContentBlock,
) : StreamChatResponse

@Serializable
@SerialName("content_block_delta")
data class ContentBlock(
override val type: String,
val text: String,
): StreamChatResponse

@Serializable
@SerialName("content_block_delta")
data class ContentBlockDelta(
override val type: String,
val index: Int,
val delta: Delta,
): StreamChatResponse

@Serializable
@SerialName("content_block_stop")
data class ContentBlockStop(
override val type: String,
val index: Int,
): StreamChatResponse

@Serializable
@SerialName("message_delta")
data class MessageDelta(
override val type: String,
val delta: Delta,
): StreamChatResponse

@Serializable
@SerialName("message_stop")
data class MessageStop(
override val type: String,
): StreamChatResponse

@Serializable
@SerialName("ping")
data class Ping(
override val type: String,
): StreamChatResponse

@Serializable
data class Delta(
val type: String,
val text: String,
)


Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.tddworks.anthropic.di

import com.tddworks.anthropic.api.messages.api.internal.JsonLenient
import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module

expect fun platformModule(): Module

fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) =
startKoin {
appDeclaration()
modules(commonModule(enableNetworkLogs = enableNetworkLogs))
}

fun commonModule(enableNetworkLogs: Boolean) = module {
singleOf(::createJson)
}

fun createJson() = JsonLenient
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tddworks.anthropic.di

import org.koin.core.module.Module

actual fun platformModule(): Module {
TODO("Not yet implemented")
}
Loading

0 comments on commit 85934b8

Please sign in to comment.