From fef9a0dae424045ac65d0ee4da6aeecc9f8c5608 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Fri, 21 Mar 2025 06:51:49 -0400 Subject: [PATCH] Update dependencies --- .idea/kotlinc.xml | 2 +- build.gradle.kts | 72 +++++++-------- map-view/build.gradle.kts | 6 +- .../kotlin/info/mechyrdia/lore/ParserHtml.kt | 6 ++ .../kotlin/info/mechyrdia/robot/RobotApi.kt | 23 ++++- .../info/mechyrdia/robot/RobotRateLimiter.kt | 29 ++---- .../info/mechyrdia/robot/RobotService.kt | 29 +++--- .../kotlin/info/mechyrdia/robot/RobotSse.kt | 92 ------------------- .../info/mechyrdia/robot/RobotUserLimiter.kt | 6 +- 9 files changed, 87 insertions(+), 178 deletions(-) delete mode 100644 src/main/kotlin/info/mechyrdia/robot/RobotSse.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index bb44937..131e44d 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d94067f..71ddce2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,10 +24,10 @@ buildscript { plugins { java - kotlin("jvm") version "2.1.0" - kotlin("plugin.serialization") version "2.1.0" - kotlin("multiplatform") version "2.1.0" apply false - id("com.github.johnrengelman.shadow") version "8.1.1" + kotlin("jvm") version "2.1.20" + kotlin("plugin.serialization") version "2.1.20" + kotlin("multiplatform") version "2.1.20" apply false + id("com.gradleup.shadow") version "8.3.6" application } @@ -51,50 +51,50 @@ dependencies { implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.8.0") - implementation("io.ktor:ktor-server-core-jvm:3.0.2") - implementation("io.ktor:ktor-server-cio-jvm:3.0.2") + implementation("io.ktor:ktor-server-core-jvm:3.1.1") + implementation("io.ktor:ktor-server-cio-jvm:3.1.1") - implementation("io.ktor:ktor-server-auto-head-response:3.0.2") - implementation("io.ktor:ktor-server-caching-headers:3.0.2") - implementation("io.ktor:ktor-server-call-id:3.0.2") - implementation("io.ktor:ktor-server-call-logging:3.0.2") - implementation("io.ktor:ktor-server-conditional-headers:3.0.2") - implementation("io.ktor:ktor-server-content-negotiation:3.0.2") - implementation("io.ktor:ktor-server-default-headers:3.0.2") - implementation("io.ktor:ktor-server-forwarded-header:3.0.2") - implementation("io.ktor:ktor-server-html-builder:3.0.2") - implementation("io.ktor:ktor-server-resources:3.0.2") - implementation("io.ktor:ktor-server-sessions-jvm:3.0.2") - implementation("io.ktor:ktor-server-status-pages:3.0.2") - implementation("io.ktor:ktor-server-websockets:3.0.2") + implementation("io.ktor:ktor-server-auto-head-response:3.1.1") + implementation("io.ktor:ktor-server-caching-headers:3.1.1") + implementation("io.ktor:ktor-server-call-id:3.1.1") + implementation("io.ktor:ktor-server-call-logging:3.1.1") + implementation("io.ktor:ktor-server-conditional-headers:3.1.1") + implementation("io.ktor:ktor-server-content-negotiation:3.1.1") + implementation("io.ktor:ktor-server-default-headers:3.1.1") + implementation("io.ktor:ktor-server-forwarded-header:3.1.1") + implementation("io.ktor:ktor-server-html-builder:3.1.1") + implementation("io.ktor:ktor-server-resources:3.1.1") + implementation("io.ktor:ktor-server-sessions-jvm:3.1.1") + implementation("io.ktor:ktor-server-status-pages:3.1.1") + implementation("io.ktor:ktor-server-websockets:3.1.1") - implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.2") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.1") - implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0") + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0") - implementation("org.slf4j:slf4j-api:2.0.7") - implementation("ch.qos.logback:logback-classic:1.5.13") + implementation("org.slf4j:slf4j-api:2.0.16") + implementation("ch.qos.logback:logback-classic:1.5.18") implementation("com.aventrix.jnanoid:jnanoid:2.0.0") - implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0") - implementation("org.mongodb:bson-kotlinx:5.0.0") + implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.3.1") + implementation("org.mongodb:bson-kotlinx:5.3.1") implementation(files("libs/nsapi4j.jar")) - implementation("de.mkammerer:argon2-jvm:2.11") + implementation("de.mkammerer:argon2-jvm:2.12") - implementation("org.apache.groovy:groovy-jsr223:4.0.22") + implementation("org.apache.groovy:groovy-jsr223:4.0.26") - implementation("io.ktor:ktor-client-core:3.0.2") - implementation("io.ktor:ktor-client-java:3.0.2") - implementation("io.ktor:ktor-client-content-negotiation:3.0.2") - implementation("io.ktor:ktor-client-logging:3.0.2") + implementation("io.ktor:ktor-client-core:3.1.1") + implementation("io.ktor:ktor-client-java:3.1.1") + implementation("io.ktor:ktor-client-content-negotiation:3.1.1") + implementation("io.ktor:ktor-client-logging:3.1.1") implementation("com.aallam.ktoken:ktoken:0.4.0") diff --git a/map-view/build.gradle.kts b/map-view/build.gradle.kts index 4ba6a6c..e79f858 100644 --- a/map-view/build.gradle.kts +++ b/map-view/build.gradle.kts @@ -71,9 +71,9 @@ kotlin { val mapMain by getting { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.11.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.12.0") implementation(project(":externals")) } diff --git a/src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt b/src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt index 407bb5e..a70bde2 100644 --- a/src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt +++ b/src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt @@ -242,6 +242,12 @@ fun (TagConsumer<*>.(T1?, T2?, T3?, block: Tag.() -> Unit) -> Any?) } } +fun (TagConsumer<*>.(T1?, T2?, T3?, T4?, block: Tag.() -> Unit) -> Any?).toTagCreator(): TagCreator { + return { + this@toTagCreator(null, null, null, null, it) + } +} + enum class HtmlTagMode { INLINE, BLOCK, diff --git a/src/main/kotlin/info/mechyrdia/robot/RobotApi.kt b/src/main/kotlin/info/mechyrdia/robot/RobotApi.kt index d753e4d..8e9506d 100644 --- a/src/main/kotlin/info/mechyrdia/robot/RobotApi.kt +++ b/src/main/kotlin/info/mechyrdia/robot/RobotApi.kt @@ -2,6 +2,7 @@ package info.mechyrdia.robot import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.plugins.sse.sse import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.delete import io.ktor.client.request.forms.formData @@ -12,8 +13,14 @@ import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.http.parameters +import io.ktor.sse.ServerSentEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.shareIn @JvmInline value class RobotClient( @@ -88,11 +95,17 @@ value class RobotClient( suspend fun deleteThread(threadId: RobotThreadId) = client.delete("https://api.openai.com/v1/threads/${threadId.id}").body() - suspend fun createRun(threadId: RobotThreadId, assistId: RobotAssistantId, messages: List): Flow = client.postSse("https://api.openai.com/v1/threads/${threadId.id}/runs") { - val request = RobotCreateRunRequest(assistantId = assistId, additionalMessages = messages, stream = true) - setJsonBody(request) - attributes.addTokens(request) - } + suspend fun createRun(threadId: RobotThreadId, assistId: RobotAssistantId, messages: List): Flow = flow { + client.sse("https://api.openai.com/v1/threads/${threadId.id}/runs", request = { + val request = RobotCreateRunRequest(assistantId = assistId, additionalMessages = messages, stream = true) + setJsonBody(request) + attributes.addTokens(request) + }) { + incoming.collect { + emit(it) + } + } + }.shareIn(CoroutineScope(currentCoroutineContext()), SharingStarted.Lazily) } inline fun HttpRequestBuilder.setJsonBody(body: T) { diff --git a/src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt b/src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt index 91d705f..c3e0e33 100644 --- a/src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt +++ b/src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt @@ -1,13 +1,9 @@ package info.mechyrdia.robot -import com.aallam.ktoken.Encoding -import com.aallam.ktoken.Tokenizer import io.ktor.client.plugins.api.createClientPlugin import io.ktor.util.AttributeKey import io.ktor.util.Attributes import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import java.time.Instant import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -27,7 +23,7 @@ private fun String.parseDurationToSeconds(): Int { return (hours * 3600) + (minutes * 60) + seconds } -private fun Int.secondFromNow() = Instant.now().epochSecond + this +private fun Int.secondsFromNow() = Instant.now().epochSecond + this private fun calculateRateLimitDelayDouble(requestsRemaining: Int, requestsResetAt: Long): Double? { val now = Instant.now().epochSecond @@ -60,32 +56,21 @@ val RobotRateLimiter = createClientPlugin("RobotRateLimiter") { @Suppress("UastIncorrectHttpHeaderInspection") onResponse { response -> response.headers["X-Ratelimit-Remaining-Requests"]?.toIntOrNull()?.let(requestsRemaining::set) - response.headers["X-Ratelimit-Reset-Requests"]?.parseDurationToSeconds()?.secondFromNow()?.let(requestsResetAt::set) + response.headers["X-Ratelimit-Reset-Requests"]?.parseDurationToSeconds()?.secondsFromNow()?.let(requestsResetAt::set) response.headers["X-Ratelimit-Remaining-Tokens"]?.toIntOrNull()?.let(tokensRemaining::set) - response.headers["X-Ratelimit-Reset-Tokens"]?.parseDurationToSeconds()?.secondFromNow()?.let(tokensResetAt::set) + response.headers["X-Ratelimit-Reset-Tokens"]?.parseDurationToSeconds()?.secondsFromNow()?.let(tokensResetAt::set) } } private val RobotTokenCountKey = AttributeKey("Mechyrdia.RobotTokenCount") -suspend fun Attributes.addTokens(tokenizable: Tokenizable) { +fun Attributes.addTokens(tokenizable: Tokenizable) { val deltaCount = tokenizable.getTexts().countTokens() put(RobotTokenCountKey, deltaCount + computeIfAbsent(RobotTokenCountKey) { 0 }) } fun Attributes.getTokens(): Int? = getOrNull(RobotTokenCountKey) -private var tokenizerStore: Tokenizer? = null -private val tokenizerMutex = Mutex() - -suspend fun getTokenizer(): Tokenizer { - return tokenizerStore ?: tokenizerMutex.withLock { - Tokenizer.of(Encoding.CL100K_BASE).also { - tokenizerStore = it - } - } -} - fun interface Tokenizable { fun getTexts(): List } @@ -94,10 +79,10 @@ fun List.flatten() = Tokenizable { flatMap { it.getTexts() } } -suspend fun String.countTokens(): Int { - return getTokenizer().encode(this).size +fun String.countTokens(): Int { + return RobotService.tokenizer.encode(this).size } -suspend fun List.countTokens(): Int { +fun List.countTokens(): Int { return sumOf { it.countTokens() } } diff --git a/src/main/kotlin/info/mechyrdia/robot/RobotService.kt b/src/main/kotlin/info/mechyrdia/robot/RobotService.kt index 1189970..a3501d0 100644 --- a/src/main/kotlin/info/mechyrdia/robot/RobotService.kt +++ b/src/main/kotlin/info/mechyrdia/robot/RobotService.kt @@ -1,5 +1,7 @@ package info.mechyrdia.robot +import com.aallam.ktoken.Encoding +import com.aallam.ktoken.Tokenizer import info.mechyrdia.Configuration import info.mechyrdia.MainDomainName import info.mechyrdia.OpenAiConfig @@ -21,6 +23,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.sse.SSE import io.ktor.client.request.header import io.ktor.http.ContentType import io.ktor.http.HttpHeaders @@ -49,25 +52,9 @@ import kotlinx.serialization.Serializable import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Instant -import kotlin.collections.List -import kotlin.collections.Map -import kotlin.collections.Set -import kotlin.collections.buildMap import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.emptyMap -import kotlin.collections.emptySet -import kotlin.collections.flatMap -import kotlin.collections.fold -import kotlin.collections.forEach -import kotlin.collections.iterator -import kotlin.collections.listOf -import kotlin.collections.map -import kotlin.collections.minus -import kotlin.collections.mutableListOf -import kotlin.collections.plus import kotlin.collections.set -import kotlin.collections.toList import kotlin.random.Random import kotlin.time.Duration.Companion.minutes @@ -136,8 +123,12 @@ class RobotService( Logging { level = LogLevel.INFO sanitizeHeader("") { it == HttpHeaders.Authorization } + sanitizeHeader("") { it == "OpenAI-Organization" } + sanitizeHeader("") { it == "OpenAI-Project" } } + install(SSE) + install(HttpRequestRetry) { retryOnExceptionOrServerErrors(5) delayMillis { retry -> @@ -421,6 +412,9 @@ class RobotService( private val instanceHolder = CoroutineScope(CoroutineName("robot-service-initialization")).async { startInitializing.join() + + tokenizer = Tokenizer.Companion.of(Encoding.CL100K_BASE) + Configuration.Current.openAi?.let { config -> status = RobotServiceStatus.LOADING RobotService(config).apply { initialize() } @@ -437,6 +431,9 @@ class RobotService( } } + lateinit var tokenizer: Tokenizer + private set + var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED private set diff --git a/src/main/kotlin/info/mechyrdia/robot/RobotSse.kt b/src/main/kotlin/info/mechyrdia/robot/RobotSse.kt deleted file mode 100644 index cdb711f..0000000 --- a/src/main/kotlin/info/mechyrdia/robot/RobotSse.kt +++ /dev/null @@ -1,92 +0,0 @@ -package info.mechyrdia.robot - -import io.ktor.client.HttpClient -import io.ktor.client.request.HttpRequestBuilder -import io.ktor.client.request.prepareGet -import io.ktor.client.request.preparePost -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsChannel -import io.ktor.utils.io.readUTF8Line -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.flow - -data class ServerSentEvent( - val data: String?, - val event: String?, - val id: String?, - val retry: Double?, -) - -private class SseBuilder { - var data: String? = null - var event: String? = null - var id: String? = null - var retry: Double? = null - - fun build() = ServerSentEvent(data, event, id, retry) - - val isSet: Boolean - get() = data != null || event != null || id != null || retry != null - - fun reset() { - data = "" - event = null - id = null - retry = null - } -} - -private const val SSE_DATA_PREFIX = "data: " -private const val SSE_EVENT_PREFIX = "event: " -private const val SSE_ID_PREFIX = "id: " -private const val SSE_RETRY_PREFIX = "retry: " - -private suspend fun FlowCollector.receiveSse(response: HttpResponse) { - val reader = response.bodyAsChannel() - val builder = SseBuilder() - while (true) { - val line = reader.readUTF8Line() ?: break - - if (line.isBlank()) { - if (builder.isSet) - emit(builder.build()) - builder.reset() - continue - } - - if (line.startsWith(":")) continue - - if (line.startsWith(SSE_DATA_PREFIX)) - builder.data = builder.data?.let { "$it\n" }.orEmpty() + line.substring(SSE_DATA_PREFIX.length) - if (line.startsWith(SSE_EVENT_PREFIX)) - builder.event = line.substring(SSE_EVENT_PREFIX.length) - if (line.startsWith(SSE_ID_PREFIX)) - builder.id = line.substring(SSE_ID_PREFIX.length) - if (line.startsWith(SSE_RETRY_PREFIX)) - builder.retry = line.substring(SSE_RETRY_PREFIX.length).toDoubleOrNull() - } - - if (builder.isSet) - emit(builder.build()) -} - -fun HttpClient.getSse(urlString: String, requestBuilder: suspend HttpRequestBuilder.() -> Unit): Flow { - return flow { - prepareGet(urlString) { - requestBuilder() - }.execute { response -> - receiveSse(response) - } - } -} - -fun HttpClient.postSse(urlString: String, requestBuilder: suspend HttpRequestBuilder.() -> Unit): Flow { - return flow { - preparePost(urlString) { - requestBuilder() - }.execute { response -> - receiveSse(response) - } - } -} diff --git a/src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt b/src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt index 965b366..c1a5ff1 100644 --- a/src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt +++ b/src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt @@ -75,15 +75,15 @@ class ConversationMessageTokenTracker { fun addMessage(message: RobotConversationMessage) { when (message) { - is RobotConversationMessage.User -> request.append(message.text) - is RobotConversationMessage.Robot -> response.append(message.text) + is RobotConversationMessage.User -> request.append(message.text).append(' ') + is RobotConversationMessage.Robot -> response.append(message.text).append(' ') else -> { // ignore } } } - suspend fun calculateTokens(): Int { + fun calculateTokens(): Int { return (request.toString().countTokens() * REQUEST_TOKEN_WEIGHT) + (response.toString().countTokens() * RESPONSE_TOKEN_WEIGHT) } } -- 2.25.1