Skip to content

Commit

Permalink
feat(BE-174): integrate anthropic api
Browse files Browse the repository at this point in the history
  • Loading branch information
hanrw committed Mar 14, 2024
1 parent a44fa05 commit 70b176e
Show file tree
Hide file tree
Showing 31 changed files with 315 additions and 82 deletions.
45 changes: 45 additions & 0 deletions anthropic-client/anthropic-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
plugins {
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kover)
// `maven-publish`
}

kotlin {
jvm()
macosArm64()
macosX64()

sourceSets {
commonMain.dependencies {
// put your Multiplatform dependencies here
api(libs.kotlinx.serialization.json)
api(libs.bundles.ktor.client)
}

commonTest.dependencies {
implementation(libs.ktor.client.mock)
}

macosMain.dependencies {
api(libs.ktor.client.darwin)
}

jvmMain.dependencies {
api(libs.ktor.client.cio)
}

jvmTest.dependencies {
implementation(project.dependencies.platform(libs.junit.bom))
implementation(libs.bundles.jvm.test)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("org.reflections:reflections:0.10.2")
}
}
}

tasks {
named<Test>("jvmTest") {
useJUnitPlatform()
}
}
15 changes: 15 additions & 0 deletions anthropic-client/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
alias(libs.plugins.kover)
// `maven-publish`
}

kotlin {
jvm()
sourceSets {
commonMain {
dependencies {
api(projects.anthropicClient.anthropicClientCore)
}
}
}
}
47 changes: 47 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
plugins {
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kover)
// alias(libs.plugins.touchlab.kmmbridge)
// id("module.publication")
`maven-publish`
}

kotlin {
jvm()
macosArm64()
macosX64()

sourceSets {
commonMain.dependencies {
// put your Multiplatform dependencies here
api(libs.kotlinx.serialization.json)
api(libs.bundles.ktor.client)
}

commonTest.dependencies {
implementation(libs.ktor.client.mock)
}

macosMain.dependencies {
api(libs.ktor.client.darwin)
}

jvmMain.dependencies {
api(libs.ktor.client.cio)
}

jvmTest.dependencies {
implementation(project.dependencies.platform(libs.junit.bom))
implementation(libs.bundles.jvm.test)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("org.reflections:reflections:0.10.2")
}
}
}

tasks {
named<Test>("jvmTest") {
useJUnitPlatform()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.internal.network.ktor
package com.tddworks.common.network.api.ktor.api

import io.ktor.client.request.*
import io.ktor.client.statement.*
Expand Down Expand Up @@ -31,15 +31,15 @@ interface HttpRequester {
* @param builder The HttpRequestBuilder that contains the HTTP request details.
* @return The result of the HTTP request.
*/
internal suspend inline fun <reified T> HttpRequester.performRequest(noinline builder: HttpRequestBuilder.() -> Unit): T {
suspend inline fun <reified T> HttpRequester.performRequest(noinline builder: HttpRequestBuilder.() -> Unit): T {
return performRequest(typeInfo<T>(), builder)
}


/**
* Perform an HTTP request and get a result
*/
internal inline fun <reified T : Any> HttpRequester.streamRequest(
inline fun <reified T : Any> HttpRequester.streamRequest(
noinline builder: HttpRequestBuilder.() -> Unit,
): Flow<T> {
return flow {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.common
package com.tddworks.common.network.api.ktor.api

import kotlinx.serialization.Serializable

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.tddworks.common.network.api.ktor.api

import com.tddworks.common.network.api.ktor.internal.JsonLenient
import io.ktor.client.call.*
import io.ktor.client.statement.*
import io.ktor.utils.io.*
import kotlinx.coroutines.flow.FlowCollector

const val STREAM_PREFIX = "data:"
private const val STREAM_END_TOKEN = "$STREAM_PREFIX [DONE]"

/**
* Get data as [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format).
*/
suspend inline fun <reified T> FlowCollector<T>.streamEventsFrom(response: HttpResponse) {
val channel: ByteReadChannel = response.body()
while (!channel.isClosedForRead) {
channel.readUTF8Line()?.let { streamResponse ->
if (notEndStreamResponse(streamResponse)) {
emit(JsonLenient.decodeFromString(streamResponse.removePrefix(STREAM_PREFIX)))
}
} ?: break
}
}

private fun isStreamResponse(line: String) = line.startsWith(STREAM_PREFIX)

fun notEndStreamResponse(line: String) = line != STREAM_END_TOKEN && isStreamResponse(line)
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.tddworks.openai.api.internal.network.ktor
package com.tddworks.common.network.api.ktor.internal

import com.tddworks.openai.api.internal.network.ktor.exception.*
import com.tddworks.openai.api.internal.network.ktor.internal.createHttpClient
import com.tddworks.common.network.api.ktor.api.HttpRequester
import com.tddworks.common.network.api.ktor.internal.exception.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.network.sockets.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
Expand All @@ -17,7 +16,7 @@ import kotlinx.coroutines.CancellationException
* Default implementation of [HttpRequester].
* @property httpClient The HttpClient to use for performing HTTP requests.
*/
internal class DefaultHttpRequester(private val httpClient: HttpClient) : HttpRequester {
class DefaultHttpRequester(private val httpClient: HttpClient) : HttpRequester {

override suspend fun <T : Any> performRequest(info: TypeInfo, builder: HttpRequestBuilder.() -> Unit): T {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.tddworks.openai.api.internal.network.ktor.internal
package com.tddworks.common.network.api.ktor.internal

import com.tddworks.openai.api.common.JsonLenient
import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.*
Expand All @@ -20,7 +19,7 @@ import kotlin.time.Duration.Companion.minutes
* @param token the authentication token
* @return a new [HttpClient] instance
*/
internal fun <T : HttpClientEngineConfig> createHttpClient(
fun <T : HttpClientEngineConfig> createHttpClient(
url: String,
token: String,
engine: HttpClientEngineFactory<T>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.common
package com.tddworks.common.network.api.ktor.internal

import kotlinx.serialization.json.Json

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.internal.network.ktor.exception
package com.tddworks.common.network.api.ktor.internal.exception


/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.internal.network.ktor.exception
package com.tddworks.common.network.api.ktor.internal.exception

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.internal.network.ktor.exception
package com.tddworks.common.network.api.ktor.internal.exception

/** OpenAI client exception */
sealed class OpenAIException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tddworks.openai.api.internal.network.ktor.exception;
package com.tddworks.common.network.api.ktor.internal.exception;

/**
* An exception thrown in case of an I/O error
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.tddworks.openai.api.internal.network.ktor
package com.tddworks.common.network.api.ktor.internal

import com.tddworks.openai.api.internal.network.ktor.internal.createHttpClient
import com.tddworks.common.network.api.ktor.api.HttpRequester
import io.ktor.client.engine.cio.*

actual fun HttpRequester.Companion.default(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.tddworks.common.network.api

import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.reflections.Reflections
import org.reflections.scanners.Scanners
import java.io.IOException

class InternalPackageTest {

companion object {
private val BASE_PACKAGE = InternalPackageTest::class.java.`package`.name
}

private val analyzedClasses = ClassFileImporter().importPackages(BASE_PACKAGE)

@Test
@Throws(IOException::class)
fun internalPackagesAreNotAccessedFromOutside() {

// so that the test will break when the base package is re-named
assertPackageExists(BASE_PACKAGE)
val internalPackages = internalPackages(BASE_PACKAGE)
for (internalPackage in internalPackages) {
assertPackageExists(internalPackage)
assertPackageIsNotAccessedFromOutside(internalPackage)
}
}

/**
* Finds all packages named "internal".
*/
private fun internalPackages(basePackage: String): List<String> {
val scanner = Scanners.SubTypes
val reflections = Reflections(basePackage, scanner)
return reflections.getSubTypesOf(Object::class.java).map {
it.`package`.name
}.filter {
it.endsWith(".internal")
}
}

private fun assertPackageIsNotAccessedFromOutside(internalPackage: String) {
noClasses()
.that()
.resideOutsideOfPackage(packageMatcher(internalPackage))
.should()
.dependOnClassesThat()
.resideInAPackage(packageMatcher(internalPackage))
.check(analyzedClasses)
}

private fun assertPackageExists(packageName: String?) {
assertThat(analyzedClasses.containPackage(packageName))
.`as`("package %s exists", packageName)
.isTrue()
}

private fun packageMatcher(fullyQualifiedPackage: String): String? {
return "$fullyQualifiedPackage.."
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tddworks.common.network.api

import kotlinx.serialization.json.Json

val prettyJson = Json { // this returns the JsonBuilder
prettyPrint = true
ignoreUnknownKeys = true
// optional: specify indent
prettyPrintIndent = " "
}

/**
* 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..
*/
internal val JsonLenient = Json {
isLenient = true
ignoreUnknownKeys = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.tddworks.common.network.api


import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.*

/**
* See https://ktor.io/docs/http-client-testing.html#usage
*/
fun mockHttpClient(mockResponse: String) = HttpClient(MockEngine) {

val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))

install(ContentNegotiation) {
register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient))
}

engine {
addHandler { request ->
if (request.url.encodedPath == "/v1/chat/completions"
|| request.url.encodedPath == "/v1/images/generations"
) {
respond(mockResponse, HttpStatusCode.OK, headers)
} else {
error("Unhandled ${request.url.encodedPath}")
}
}
}

defaultRequest {
url {
protocol = URLProtocol.HTTPS
host = "api.lemonsqueezy.com"
}

header(HttpHeaders.ContentType, ContentType.Application.Json)
contentType(ContentType.Application.Json)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.tddworks.openai.api.internal.network.ktor
package com.tddworks.common.network.api.ktor

import com.tddworks.openai.api.common.mockHttpClient
import com.tddworks.common.network.api.ktor.internal.DefaultHttpRequester
import com.tddworks.common.network.api.ktor.api.performRequest
import com.tddworks.common.network.api.mockHttpClient
import io.ktor.client.*
import io.ktor.client.request.*
import kotlinx.coroutines.runBlocking
Expand Down
Loading

0 comments on commit 70b176e

Please sign in to comment.