<?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
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
}
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")
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"))
}
}
}
+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,
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
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(
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) {
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
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
@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>
}
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() }
}
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
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
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
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 ->
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() }
}
}
+ lateinit var tokenizer: Tokenizer
+ private set
+
var status: RobotServiceStatus = RobotServiceStatus.NOT_CONFIGURED
private set
+++ /dev/null
-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)
- }
- }
-}
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)
}
}