Update dependencies
authorLanius Trolling <lanius@laniustrolling.dev>
Fri, 21 Mar 2025 10:51:49 +0000 (06:51 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Fri, 21 Mar 2025 10:51:49 +0000 (06:51 -0400)
.idea/kotlinc.xml
build.gradle.kts
map-view/build.gradle.kts
src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt
src/main/kotlin/info/mechyrdia/robot/RobotApi.kt
src/main/kotlin/info/mechyrdia/robot/RobotRateLimiter.kt
src/main/kotlin/info/mechyrdia/robot/RobotService.kt
src/main/kotlin/info/mechyrdia/robot/RobotSse.kt [deleted file]
src/main/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt

index bb4493707fa33f413148da60b12e35a37a704010..131e44d79845abd82448872bfe845f2d19c217b0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="KotlinJpsPluginSettings">
-    <option name="version" value="2.1.0" />
+    <option name="version" value="2.1.20" />
   </component>
 </project>
\ No newline at end of file
index d94067f587ca39b3abff043d7aa335ed743fb72c..71ddce2345ce356379bbeac7565f8ffd12c7e6e4 100644 (file)
@@ -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")
        
index 4ba6a6ce1628818e7b024a6c85436694c094d322..e79f8583926efce3c8e6fe12599562ce579f1c63 100644 (file)
@@ -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"))
                        }
index 407bb5ed2cc2b529c8091587105382464e808ada..a70bde2aa3a953047f7260fe8f1e1168735792a1 100644 (file)
@@ -242,6 +242,12 @@ fun <T1, T2, T3> (TagConsumer<*>.(T1?, T2?, T3?, block: Tag.() -> Unit) -> Any?)
        }
 }
 
+fun <T1, T2, T3, T4> (TagConsumer<*>.(T1?, T2?, T3?, T4?, block: Tag.() -> Unit) -> Any?).toTagCreator(): TagCreator {
+       return {
+               this@toTagCreator(null, null, null, null, it)
+       }
+}
+
 enum class HtmlTagMode {
        INLINE,
        BLOCK,
index d753e4d6141f8868cae918898023235d373caf7d..8e9506db0c4901c4507fcb447aa59f85e979ef22 100644 (file)
@@ -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<RobotThreadDeletionResponse>()
        
-       suspend fun createRun(threadId: RobotThreadId, assistId: RobotAssistantId, messages: List<RobotCreateThreadRequestMessage>): Flow<ServerSentEvent> = 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<RobotCreateThreadRequestMessage>): Flow<ServerSentEvent> = 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 <reified T> HttpRequestBuilder.setJsonBody(body: T) {
index 91d705f3c5303153e8dda3e21341b76527b58cf1..c3e0e3349627686a9782308afb6dc43a4c5c7d36 100644 (file)
@@ -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<Int>("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<String>
 }
@@ -94,10 +79,10 @@ fun List<Tokenizable>.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<String>.countTokens(): Int {
+fun List<String>.countTokens(): Int {
        return sumOf { it.countTokens() }
 }
index 11899704abca9e277274a75ac93b669f67d17176..a3501d0846c97e8bebbd41cdc344875a66047faf 100644 (file)
@@ -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("<OPENAI TOKEN>") { it == HttpHeaders.Authorization }
+                               sanitizeHeader("<OPENAI ORG ID>") { it == "OpenAI-Organization" }
+                               sanitizeHeader("<OPENAI PROJECT ID>") { 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 (file)
index cdb711f..0000000
+++ /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<ServerSentEvent>.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<ServerSentEvent> {
-       return flow {
-               prepareGet(urlString) {
-                       requestBuilder()
-               }.execute { response ->
-                       receiveSse(response)
-               }
-       }
-}
-
-fun HttpClient.postSse(urlString: String, requestBuilder: suspend HttpRequestBuilder.() -> Unit): Flow<ServerSentEvent> {
-       return flow {
-               preparePost(urlString) {
-                       requestBuilder()
-               }.execute { response ->
-                       receiveSse(response)
-               }
-       }
-}
index 965b3663bfac0faa8d312b9bc12caabe28dfeeb5..c1a5ff1f4bfb832aa04b2414c7e274b890a5d15b 100644 (file)
@@ -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)
        }
 }