Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions ai-mocks-gemini/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ kotlin {
commonMain {
dependencies {
api(libs.ktor.serialization.kotlinx.json)
api(libs.ktor.sse)
api(project(":ai-mocks-core"))
api(project.dependencies.platform(libs.ktor.bom))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package me.kpavlov.aimocks.gemini.content

import io.ktor.sse.TypedServerSentEvent
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -84,22 +86,22 @@ public class GeminiStreamingContentBuildingStep(
}
}

@OptIn(InternalAPI::class)
private fun encodeChunk(
chunk: GenerateContentResponse,
sse: Boolean,
lastChunk: Boolean = false,
): String {
val json =
Json.encodeToString(
value = chunk,
serializer = GenerateContentResponse.serializer(),
)
return if (sse) {
"data: $json\r\n\r\n"
TypedServerSentEvent(
data = chunk,
).toString {
Json.encodeToString(it)
}
} else if (lastChunk) {
json
Json.encodeToString(value = chunk)
} else {
"$json,\r\n"
"${Json.encodeToString(value = chunk)},\r\n"
}
}

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ ktor-server-core = { module = "io.ktor:ktor-server-core" }
ktor-server-double-receive = { module = "io.ktor:ktor-server-double-receive" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty" }
ktor-server-sse = { module = "io.ktor:ktor-server-sse" }
ktor-sse = { module = "io.ktor:ktor-sse" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

io.ktor:ktor-sse artifact doesn’t exist in Ktor 3.3.0
Ktor only publishes ktor-server-sse / ktor-client-sse. This alias will break dependency resolution as Maven can’t find io.ktor:ktor-sse. Please point the alias to the actual module you need (e.g., ktor-client-sse) or drop it.

🤖 Prompt for AI Agents
In gradle/libs.versions.toml around line 67, the alias ktor-sse is pointing to a
non-existent artifact "io.ktor:ktor-sse"; replace it with the correct module
published by Ktor 3.3.0 (for example "io.ktor:ktor-client-sse" or
"io.ktor:ktor-server-sse" depending on whether you need client or server SSE) or
remove the alias entirely so dependency resolution won't fail.

langchain4j-anthropic = { group = "dev.langchain4j", name = "langchain4j-anthropic" }
langchain4j-bom = { group = "dev.langchain4j", name = "langchain4j-bom", version.ref = "langchain4j" }
langchain4j-gemini = { group = "dev.langchain4j", name = "langchain4j-google-ai-gemini" }
Expand Down
20 changes: 15 additions & 5 deletions mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/BuildingStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.sse.ServerSentEventMetadata
import me.kpavlov.mokksy.request.RequestSpecification
import me.kpavlov.mokksy.response.ResponseDefinitionBuilder
import me.kpavlov.mokksy.response.StreamingResponseDefinitionBuilder
import me.kpavlov.mokksy.utils.logger.HttpFormatter
import java.io.IOException
import kotlin.reflect.KClass

Expand All @@ -25,6 +26,7 @@ public class BuildingStep<P : Any> internal constructor(
private val configuration: StubConfiguration,
private val requestSpecification: RequestSpecification<P>,
private val registerStub: (Stub<*, *>) -> Unit,
private val formatter: HttpFormatter,
) {
/**
* @param P The type of the request payload.
Expand All @@ -37,11 +39,13 @@ public class BuildingStep<P : Any> internal constructor(
name: String?,
requestSpecification: RequestSpecification<P>,
registerStub: (Stub<*, *>) -> Unit,
formatter: HttpFormatter,
) : this(
requestType = requestType,
configuration = StubConfiguration(name),
requestSpecification = requestSpecification,
registerStub = registerStub,
formatter = formatter,
)

/**
Expand All @@ -62,8 +66,10 @@ public class BuildingStep<P : Any> internal constructor(
val req = CapturedRequest(call.request, requestType)
@SuppressWarnings("TooGenericExceptionCaught")
try {
ResponseDefinitionBuilder<P, T>(request = req)
.apply(block)
ResponseDefinitionBuilder<P, T>(
request = req,
formatter = formatter,
).apply(block)
.build()
} catch (e: Exception) {
if (e as? IOException == null) {
Expand Down Expand Up @@ -91,15 +97,19 @@ public class BuildingStep<P : Any> internal constructor(
* @param block A lambda function applied to a [me.kpavlov.mokksy.response.StreamingResponseDefinitionBuilder],
* used to configure the streaming response definition.
*/
public infix fun <T : Any> respondsWithStream(block: StreamingResponseDefinitionBuilder<P, T>.() -> Unit) {
public infix fun <T : Any> respondsWithStream(
block: StreamingResponseDefinitionBuilder<P, T>.() -> Unit,
) {
val stub =
Stub(
configuration = configuration,
requestSpecification = requestSpecification,
) { call ->
val req = CapturedRequest(call.request, requestType)
StreamingResponseDefinitionBuilder<P, T>(request = req)
.apply(block)
StreamingResponseDefinitionBuilder<P, T>(
request = req,
formatter = formatter,
).apply(block)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ public open class MokksyServer
requestSpecification = requestSpec,
registerStub = this::registerStub,
requestType = requestType,
formatter = httpFormatter,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@ import kotlin.time.Duration
internal typealias ResponseDefinitionSupplier<T> = (
ApplicationCall,
) -> AbstractResponseDefinition<T>

/**
* Represents the base definition of an HTTP response in a mapping between a request and its corresponding response.
* Provides the required attributes and behavior for configuring HTTP responses, including status code, headers,
* and content type. This class serves as the foundation for more specialized response definitions.
*
* @param T The type of the response data.
* @property contentType The MIME type of the response content. Defaults to `null`.
* @property contentType The MIME type of the response content.
* @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200.
* @property httpStatus The HTTP status code of the response. Defaults to [HttpStatusCode.OK].
* @property headers A lambda function for configuring the response headers. Defaults to `null`.
* @property headerList A list of header key-value pairs to populate the response headers. Defaults to an empty list.
*/
public abstract class AbstractResponseDefinition<T>(
public val contentType: ContentType? = null,
public val contentType: ContentType,
public val httpStatusCode: Int = 200,
public val httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode),
public val headers: (ResponseHeaders.() -> Unit)? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.log
import io.ktor.server.request.httpVersion
import io.ktor.server.response.ResponseHeaders
import io.ktor.server.response.respond
import io.ktor.util.cio.ChannelWriteException
import kotlinx.coroutines.delay
import me.kpavlov.mokksy.utils.logger.HttpFormatter
import kotlin.time.Duration

/**
Expand All @@ -33,6 +35,7 @@ public open class ResponseDefinition<P, T>(
headers: (ResponseHeaders.() -> Unit)? = null,
headerList: List<Pair<String, String>> = emptyList<Pair<String, String>>(),
delay: Duration,
private val formatter: HttpFormatter,
) : AbstractResponseDefinition<T>(
contentType = contentType,
httpStatusCode = httpStatusCode,
Expand All @@ -49,7 +52,17 @@ public open class ResponseDefinition<P, T>(
delay(delay)
}
if (verbose) {
call.application.log.debug("Sending {}: {}", httpStatus, body)
call.application.log.debug(
"Sending:\n---\n${
formatter.formatResponse(
httpVersion = call.request.httpVersion,
headers = call.response.headers,
contentType = this.contentType,
status = httpStatus,
body = body?.toString(),
)
}---\n",
)
}
try {
call.respond(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.response.ResponseHeaders
import kotlinx.coroutines.flow.Flow
import me.kpavlov.mokksy.CapturedRequest
import me.kpavlov.mokksy.utils.logger.HttpFormatter
import java.util.Collections
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
Expand Down Expand Up @@ -66,8 +67,7 @@ public abstract class AbstractResponseDefinitionBuilder<P, T>(
*
* @param P The type of the request body.
* @param T The type of the response body.
* @property contentType Optional MIME type of the response.
* Defaults to `ContentType.Application.Json` if not specified.
* @property contentType Optional MIME type of the response. Defaults to `null` if not specified.
* @property body The body of the response. Can be null.
* @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200.
* @property httpStatus The HTTP status code of the response, defaulting to [HttpStatusCode.OK].
Expand All @@ -83,6 +83,7 @@ public open class ResponseDefinitionBuilder<P : Any, T : Any>(
httpStatusCode: Int = 200,
httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode),
headers: MutableList<Pair<String, String>> = mutableListOf(),
private val formatter: HttpFormatter,
) : AbstractResponseDefinitionBuilder<P, T>(
httpStatusCode = httpStatusCode,
httpStatus = httpStatus,
Expand All @@ -95,8 +96,9 @@ public open class ResponseDefinitionBuilder<P : Any, T : Any>(
httpStatusCode = httpStatusCode,
httpStatus = httpStatus,
headers = headersLambda,
headerList = Collections.unmodifiableList(headers),
headerList = headers.toList(),
delay = delay,
formatter = formatter,
)
}

Expand All @@ -120,7 +122,12 @@ public open class StreamingResponseDefinitionBuilder<P : Any, T>(
public var delayBetweenChunks: Duration = Duration.ZERO,
httpStatus: HttpStatusCode = HttpStatusCode.OK,
headers: MutableList<Pair<String, String>> = mutableListOf(),
) : AbstractResponseDefinitionBuilder<P, T>(httpStatus = httpStatus, headers = headers) {
public val chunkContentType: ContentType? = null,
private val formatter: HttpFormatter,
) : AbstractResponseDefinitionBuilder<P, T>(
httpStatus = httpStatus,
headers = headers,
) {
/**
* Builds an instance of `StreamResponseDefinition`.
*
Expand All @@ -142,5 +149,7 @@ public open class StreamingResponseDefinitionBuilder<P : Any, T>(
headerList = Collections.unmodifiableList(headers),
delayBetweenChunks = delayBetweenChunks,
delay = delay,
formatter = formatter,
chunkContentType = chunkContentType,
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.mokksy.response

import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
Expand All @@ -14,12 +15,15 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emptyFlow
import me.kpavlov.mokksy.utils.logger.HttpFormatter
import kotlin.time.Duration

public open class SseStreamResponseDefinition<P>(
override val chunkFlow: Flow<ServerSentEvent>? = null,
private val chunkContentType: ContentType? = null,
delay: Duration = Duration.ZERO,
) : StreamResponseDefinition<P, ServerSentEvent>(delay = delay) {
formatter: HttpFormatter,
) : StreamResponseDefinition<P, ServerSentEvent>(delay = delay, formatter = formatter) {
override suspend fun writeResponse(
call: ApplicationCall,
verbose: Boolean,
Expand Down
Loading
Loading