From 21f58945d495a1f7b33152847da1038a6b0120a3 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sat, 25 Nov 2023 19:28:29 -0500 Subject: [PATCH] 1.0.39 (#43) * 1.0.39 * 1.0.39 * wip * misc * Update ActorOptTest.kt * misc * misc * fixes * AWSSDK2 * FailedToImplementException * Update DataStorage.kt * wip --- README.md | 6 +- core/build.gradle.kts | 19 +- .../com/simiacryptus/skyenet/core/Brain.kt | 216 --- .../skyenet/core/{Heart.kt => Interpreter.kt} | 5 +- .../com/simiacryptus/skyenet/core/Mouth.kt | 69 - .../skyenet/core/actors/ActorSystem.kt | 4 +- .../skyenet/core/actors/BaseActor.kt | 28 +- .../skyenet/core/actors/CodingActor.kt | 505 +++--- .../skyenet/core/actors/ImageActor.kt | 27 +- .../skyenet/core/actors/ParsedActor.kt | 26 +- .../skyenet/core/actors/SimpleActor.kt | 20 +- .../core/actors/opt/ActorOptimization.kt | 19 +- .../actors/record/CodingActorInterceptor.kt | 80 +- .../actors/record/ImageActorInterceptor.kt | 18 +- .../actors/record/ParsedActorInterceptor.kt | 28 +- .../actors/record/SimpleActorInterceptor.kt | 16 +- .../skyenet/core/actors/test/ActorTestBase.kt | 27 +- .../core/actors/test/CodingActorTestBase.kt | 10 +- .../core/actors/test/ImageActorTestBase.kt | 5 +- .../core/actors/test/ParsedActorTestBase.kt | 4 +- .../core/platform/AuthenticationManager.kt | 7 +- .../core/platform/AuthorizationManager.kt | 2 +- .../skyenet/core/platform/ClientManager.kt | 16 +- .../skyenet/core/platform/DataStorage.kt | 30 +- .../simiacryptus/skyenet/core/util/AwsUtil.kt | 40 +- ...eartTestBase.kt => InterpreterTestBase.kt} | 6 +- .../skyenet/core/actors/ActorOptTest.kt | 3 +- gradle.properties | 2 +- .../skyenet/groovy/GroovyInterpreter.kt | 6 +- .../skyenet/groovy/GroovyInterpreterTest.kt | 4 +- .../skyenet/kotlin/KotlinInterpreter.kt | 38 +- .../skyenet/kotlin/KotlinInterpreterTest.kt | 4 +- .../skyenet/scala/ScalaLocalInterpreter.scala | 11 +- .../scala/ScalaLocalInterpreterTest.scala | 8 +- webui/build.gradle.kts | 2 +- .../webui/application/ApplicationDirectory.kt | 10 +- .../webui/application/ApplicationInterface.kt | 15 +- .../webui/application/ApplicationServer.kt | 1 - .../application/ApplicationSocketManager.kt | 4 +- .../skyenet/webui/chat/ChatSocket.kt | 2 +- .../skyenet/webui/chat/ChatSocketManager.kt | 4 +- .../skyenet/webui/servlet/OAuthBase.kt | 7 + ...AuthenticatedWebsite.kt => OAuthGoogle.kt} | 90 +- .../skyenet/webui/servlet/SessionIdFilter.kt | 33 + .../webui/servlet/SessionListServlet.kt | 2 +- .../skyenet/webui/servlet/WelcomeServlet.kt | 2 +- .../{SessionMessage.kt => SessionTask.kt} | 6 +- .../webui/session/SocketManagerBase.kt | 18 +- .../skyenet/webui/test/CodingActorTestApp.kt | 10 +- .../skyenet/webui/test/ImageActorTestApp.kt | 5 +- .../skyenet/webui/test/ParsedActorTestApp.kt | 4 +- .../skyenet/webui/test/SimpleActorTestApp.kt | 4 +- .../src/main/resources/application/index.html | 2 +- webui/src/main/resources/application/main.js | 2 +- webui/src/main/resources/welcome/favicon.png | Bin 0 -> 57618 bytes webui/src/main/resources/welcome/favicon.svg | 1399 ++++++++--------- webui/src/main/resources/welcome/main.js | 2 +- .../skyenet/webui/ActorTestAppServer.kt | 4 +- 58 files changed, 1354 insertions(+), 1583 deletions(-) delete mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/core/Brain.kt rename core/src/main/kotlin/com/simiacryptus/skyenet/core/{Heart.kt => Interpreter.kt} (93%) delete mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/core/Mouth.kt rename core/src/main/kotlin/com/simiacryptus/skyenet/core/util/{HeartTestBase.kt => InterpreterTestBase.kt} (95%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthBase.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/{AuthenticatedWebsite.kt => OAuthGoogle.kt} (58%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionIdFilter.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/{SessionMessage.kt => SessionTask.kt} (94%) create mode 100644 webui/src/main/resources/welcome/favicon.png diff --git a/README.md b/README.md index 639b6118..ed77c616 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.38 + 1.0.39 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.38' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.39' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.38") +implementation("com.simiacryptus:skyenet:1.0.39") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index dac2105a..31c2bbe7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -28,14 +28,19 @@ kotlin { val junit_version = "5.10.1" val logback_version = "1.4.11" +val jackson_version = "2.15.3" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.35") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.36") implementation(group = "org.slf4j", name = "slf4j-api", version = "2.0.9") implementation(group = "commons-io", name = "commons-io", version = "2.15.0") + implementation(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = jackson_version) + implementation(group = "com.fasterxml.jackson.core", name = "jackson-annotations", version = jackson_version) + implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = jackson_version) + compileOnlyApi(kotlin("stdlib")) implementation(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "1.7.3") testImplementation(kotlin("stdlib")) @@ -46,16 +51,16 @@ dependencies { compileOnlyApi(group = "org.junit.jupiter", name = "junit-jupiter-api", version = junit_version) compileOnlyApi(group = "org.junit.jupiter", name = "junit-jupiter-engine", version = junit_version) - compileOnlyApi(group = "com.google.cloud", name = "google-cloud-texttospeech", version = "2.28.0") - compileOnlyApi(group = "com.amazonaws", name = "aws-java-sdk", version = "1.12.587") + compileOnlyApi(platform("software.amazon.awssdk:bom:2.21.29")) + compileOnlyApi(group = "software.amazon.awssdk", name = "aws-sdk-java", version = "2.21.9") + testImplementation(platform("software.amazon.awssdk:bom:2.21.29")) + testImplementation(group = "software.amazon.awssdk", name = "aws-sdk-java", version = "2.21.9") + compileOnlyApi(group = "ch.qos.logback", name = "logback-classic", version = logback_version) compileOnlyApi(group = "ch.qos.logback", name = "logback-core", version = logback_version) - - testImplementation(group = "com.google.cloud", name = "google-cloud-texttospeech", version = "2.28.0") - testImplementation(group = "com.amazonaws", name = "aws-java-sdk", version = "1.12.587") testImplementation(group = "ch.qos.logback", name = "logback-classic", version = logback_version) testImplementation(group = "ch.qos.logback", name = "logback-core", version = logback_version) - //mockito + testImplementation(group = "org.mockito", name = "mockito-core", version = "5.7.0") } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/Brain.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/Brain.kt deleted file mode 100644 index dcf10300..00000000 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/Brain.kt +++ /dev/null @@ -1,216 +0,0 @@ -@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - -package com.simiacryptus.skyenet.core - -import com.simiacryptus.jopenai.ApiModel.* -import com.simiacryptus.jopenai.ClientUtil.toContentList -import com.simiacryptus.jopenai.OpenAIClient -import com.simiacryptus.jopenai.describe.TypeDescriber -import com.simiacryptus.jopenai.describe.YamlDescriber -import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.OpenAITextModel -import com.simiacryptus.jopenai.util.JsonUtil.toJson -import org.intellij.lang.annotations.Language -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.util.concurrent.atomic.AtomicInteger - -open class Brain( - val api: OpenAIClient, - val symbols: java.util.Map = java.util.HashMap() as java.util.Map, - val model: OpenAITextModel = ChatModels.GPT35Turbo, - private val verbose: Boolean = false, - val temperature: Double = 0.3, - val describer: TypeDescriber = YamlDescriber(), - val language: String = "Kotlin", - private val moderated: Boolean = true, - private val apiDescription: String = apiDescription(symbols, describer), -) { - private val totalInputLength = AtomicInteger(0) - private val totalOutputLength = AtomicInteger(0) - private val totalApiDescriptionLength: AtomicInteger = AtomicInteger(0) - - open fun implement(vararg prompt: String): String { - if (verbose) log.info("Prompt: \n\t" + prompt.joinToString("\n\t")) - return implement(*(getChatSystemMessages(apiDescription) + - prompt.map { ChatMessage(Role.user, it.toContentList()) }).toTypedArray() - ) - } - - fun implement( - vararg messages: ChatMessage - ): String { - var request = ChatRequest() - request = request.copy(messages = ArrayList(messages.toList())) - totalApiDescriptionLength.addAndGet(apiDescription.length) - return chat(request) - } - - @Language("TEXT") - open fun getChatSystemMessages(apiDescription: String): List = listOf( - ChatMessage( - Role.system, """ - |You will translate natural language instructions into - |an implementation using $language and the script context. - |Use ``` code blocks labeled with $language where appropriate. - |Defined symbols include ${symbols.keySet().joinToString(", ")}. - |The runtime context is described below: - | - |$apiDescription - |""".trimMargin().trim().toContentList() - ) - ) - - fun fixCommand( - previousCode: String, - error: Throwable, - output: String, - vararg promptMessages: ChatMessage - ): Pair>> { - val request = ChatRequest( - messages = ArrayList( - promptMessages.toList() + listOf( - ChatMessage( - Role.assistant, - """ - |```${language.lowercase()} - |${previousCode} - |``` - |""".trimMargin().trim().toContentList() - ), - ChatMessage( - Role.system, - """ - |The previous code failed with the following error: - | - |``` - |${error.message?.trim() ?: ""} - |``` - | - |Output: - |``` - |${output.trim()} - |``` - | - |Correct the code and try again. - |""".trimMargin().trim().toContentList() - ) - )) - ) - totalApiDescriptionLength.addAndGet(apiDescription.length) - val response = chat(request) - val codeBlocks = extractCodeBlocks(response) - return Pair(response, codeBlocks) - } - - private fun chat(_request: ChatRequest): String { - val request = _request.copy(model = model.modelName, temperature = temperature) - val json = toJson(request) - if (moderated) api.moderate(json) - totalInputLength.addAndGet(json.length) - val chatResponse = api.chat(request, model) - var response = chatResponse.choices.first().message?.content.orEmpty() - if (verbose) log.info(response) - totalOutputLength.addAndGet(response.length) - response = response.trim() - return response - } - - companion object { - private val log = org.slf4j.LoggerFactory.getLogger(Brain::class.java) - fun String.indent(indent: String = " ") = this.replace("\n", "\n$indent") - private fun joinYamlList(typeDescriptions: List) = typeDescriptions.joinToString("\n") { - "- " + it.indent() - } - - private fun Method.superMethod(): Method? { - val superMethod = declaringClass.superclasses.flatMap { it.methods.toList() } - .find { it.name == name && it.parameters.size == parameters.size } - return superMethod?.superMethod() ?: superMethod - } - - private val Class.superclasses: List> - get() { - val superclass = superclass - val supers = if (superclass == null) listOf() - else superclass.superclasses + listOf(superclass) - return (interfaces.toList() + supers).distinct() - } - - fun apiDescription(hands: java.util.Map, yamlDescriber: TypeDescriber): String { - val types = ArrayList>() - - val apiobjs = hands.entrySet().map { (name, utilityObj) -> - val clazz = Class.forName(utilityObj.javaClass.typeName) - val methods = clazz.methods - .filter { Modifier.isPublic(it.modifiers) } - .filter { it.declaringClass == clazz } - .filter { !it.isSynthetic } - .map { it.superMethod() ?: it } - .filter { it.declaringClass != Object::class.java } - types.addAll(methods.flatMap { (listOf(it.returnType) + it.parameters.map { it.type }).filter { it != clazz } }) - types.addAll(clazz.declaredClasses.filter { Modifier.isPublic(it.modifiers) }) - """ - |$name: - | operations: - | ${joinYamlList(methods.map { yamlDescriber.describe(it) }).indent().indent()} - |""".trimMargin().trim() - }.toTypedArray() - val typeDescriptions = types - .filter { !it.isPrimitive } - .filter { !it.isSynthetic } - .filter { !it.name.startsWith("java.") } - .filter { !setOf("void").contains(it.name) } - .distinct().map { - """ - |${it.simpleName}: - | ${yamlDescriber.describe(it).indent()} - """.trimMargin().trim() - }.toTypedArray() - return """ - |api_objects: - | ${apiobjs.joinToString("\n").indent()} - |components: - | schemas: - | ${typeDescriptions.joinToString("\n").indent().indent()} - """.trimMargin() - } - - fun extractCodeBlocks(response: String): List> { - val codeBlockRegex = Regex("(?s)```(.*?)\\n(.*?)```") - val languageRegex = Regex("([a-zA-Z0-9-_]+)") - - val result = mutableListOf>() - var startIndex = 0 - - val matches = codeBlockRegex.findAll(response) - if (matches.count() == 0) return listOf(Pair("text", response)) - for (match in matches) { - // Add non-code block before the current match as "text" - if (startIndex < match.range.first) { - result.add(Pair("text", response.substring(startIndex, match.range.first))) - } - - // Extract language and code - val languageMatch = languageRegex.find(match.groupValues[1]) - val language = languageMatch?.groupValues?.get(0) ?: "code" - val code = match.groupValues[2] - - // Add code block to the result - result.add(Pair(language, code)) - - // Update the start index - startIndex = match.range.last + 1 - } - - // Add any remaining non-code text after the last code block as "text" - if (startIndex < response.length) { - result.add(Pair("text", response.substring(startIndex))) - } - - return result - } - - } - -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/Heart.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/Interpreter.kt similarity index 93% rename from core/src/main/kotlin/com/simiacryptus/skyenet/core/Heart.kt rename to core/src/main/kotlin/com/simiacryptus/skyenet/core/Interpreter.kt index 68877dc4..9da6422e 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/Heart.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/Interpreter.kt @@ -1,8 +1,9 @@ package com.simiacryptus.skyenet.core -interface Heart { +interface Interpreter { fun getLanguage(): String + fun symbols() : Map fun run(code: String): Any? fun validate(code: String): Throwable? @@ -18,7 +19,7 @@ interface Heart { fun square(x: Int): Int } @JvmStatic - fun test(factory: java.util.function.Function, Heart>) { + fun test(factory: java.util.function.Function, Interpreter>) { val testImpl = object : TestInterface { override fun square(x: Int): Int = x * x } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/Mouth.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/Mouth.kt deleted file mode 100644 index e1b9d6d9..00000000 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/Mouth.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.simiacryptus.skyenet.core - -import com.google.auth.oauth2.GoogleCredentials -import com.google.cloud.texttospeech.v1.* -import com.google.protobuf.ByteString -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import java.io.FileInputStream -import javax.sound.sampled.AudioFormat -import javax.sound.sampled.AudioSystem -import javax.sound.sampled.DataLine -import javax.sound.sampled.SourceDataLine - -/** - * The mouth is the interface to the Google Text-to-Speech API for the SkyeNet system - */ -@Suppress("unused") -open class Mouth( - private val keyfile: String -) { - - open fun speak(text: String) { - runBlocking { - synthesizeAndPlay("""$text""") - } - } - - protected open val client: TextToSpeechClient by lazy { - val credentials = - GoogleCredentials.fromStream(FileInputStream(keyfile)) - TextToSpeechClient.create(TextToSpeechSettings.newBuilder().setCredentialsProvider { credentials }.build()) - } - - open suspend fun synthesizeAndPlay(ssml: String) { - playAudio(synthesize(ssml).toByteArray()) - } - - open suspend fun synthesize(ssml: String): ByteString { - val input = SynthesisInput.newBuilder().setSsml(ssml).build() - val voice = VoiceSelectionParams.newBuilder() - .setLanguageCode("en-US") - .setSsmlGender(SsmlVoiceGender.FEMALE) - .build() - val audioConfig = AudioConfig.newBuilder() - .setAudioEncoding(AudioEncoding.LINEAR16) - .build() - val audioContent = withContext(Dispatchers.IO) { - client.synthesizeSpeech(input, voice, audioConfig) - }.audioContent - return audioContent - } - - open fun playAudio(audioData: ByteArray) { - val audioFormat = AudioFormat(22050F, 16, 1, true, false) - val info = DataLine.Info(SourceDataLine::class.java, audioFormat) - val sourceDataLine = AudioSystem.getLine(info) as SourceDataLine - try { - sourceDataLine.open(audioFormat) - sourceDataLine.start() - val wavHeaderSize = 44 // The size of a standard WAV header is 44 bytes - sourceDataLine.write(audioData, wavHeaderSize, audioData.size - wavHeaderSize) - sourceDataLine.drain() - sourceDataLine.stop() - } finally { - sourceDataLine.close() - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt index ab26dd5f..eb4d5917 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ActorSystem.kt @@ -12,13 +12,13 @@ import com.simiacryptus.skyenet.core.util.JsonFunctionRecorder import java.io.File open class ActorSystem>( - private val actors: Map>, + private val actors: Map>, val dataStorage: DataStorage, val user: User?, val session: Session ) { private val sessionDir = dataStorage.getSessionDir(user, session) - fun getActor(actor: T): BaseActor<*> { + fun getActor(actor: T): BaseActor<*,*> { val wrapper = getWrapper(actor.name) return when (val baseActor = actors[actor]) { null -> throw RuntimeException("No actor for $actor") diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt index 8a38d49e..d7ee8d34 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt @@ -1,40 +1,28 @@ package com.simiacryptus.skyenet.core.actors -import com.fasterxml.jackson.annotation.JsonIgnore import com.simiacryptus.jopenai.API -import com.simiacryptus.jopenai.ClientUtil.toContentList +import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.OpenAIModel -import com.simiacryptus.jopenai.models.OpenAITextModel -abstract class BaseActor( +abstract class BaseActor( open val prompt: String, val name: String? = null, val model: ChatModels = ChatModels.GPT35Turbo, val temperature: Double = 0.3, ) { - abstract fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API): T - open fun response(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, model: OpenAIModel = this.model, api: API) = (api as OpenAIClient).chat( - com.simiacryptus.jopenai.ApiModel.ChatRequest( - messages = ArrayList(messages.toList()), + abstract fun answer(vararg messages: ApiModel.ChatMessage, input: I, api: API): R + open fun response(vararg input: ApiModel.ChatMessage, model: OpenAIModel = this.model, api: API) = (api as OpenAIClient).chat( + ApiModel.ChatRequest( + messages = ArrayList(input.toList()), temperature = temperature, model = this.model.modelName, ), model = this.model ) - open fun answer(vararg questions: String, api: API): T = answer(*chatMessages(*questions), api = api) + open fun answer(input: I, api: API): R = answer(*chatMessages(input), input=input, api = api) - open fun chatMessages(vararg questions: String) = arrayOf( - com.simiacryptus.jopenai.ApiModel.ChatMessage( - role = com.simiacryptus.jopenai.ApiModel.Role.system, - content = prompt.toContentList() - ), - ) + questions.map { - com.simiacryptus.jopenai.ApiModel.ChatMessage( - role = com.simiacryptus.jopenai.ApiModel.Role.user, - content = it.toContentList() - ) - } + abstract fun chatMessages(questions: I): Array } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt index d647da9a..746d9e65 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt @@ -8,17 +8,14 @@ import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.describe.AbbrevWhitelistYamlDescriber import com.simiacryptus.jopenai.describe.TypeDescriber import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.OpenAITextModel -import com.simiacryptus.skyenet.core.Brain -import com.simiacryptus.skyenet.core.Brain.Companion.indent -import com.simiacryptus.skyenet.core.Heart +import com.simiacryptus.skyenet.core.Interpreter import com.simiacryptus.skyenet.core.OutputInterceptor +import java.util.* import javax.script.ScriptException import kotlin.reflect.KClass -@Suppress("unused", "MemberVisibilityCanBePrivate") open class CodingActor( - val interpreterClass: KClass, + val interpreterClass: KClass, val symbols: Map = mapOf(), val describer: TypeDescriber = AbbrevWhitelistYamlDescriber( "com.simiacryptus", @@ -27,34 +24,58 @@ open class CodingActor( name: String? = interpreterClass.simpleName, val details: String? = null, model: ChatModels = ChatModels.GPT35Turbo, - val fallbackModel: OpenAITextModel = ChatModels.GPT4Turbo, + val fallbackModel: ChatModels = ChatModels.GPT4Turbo, temperature: Double = 0.1, - val autoEvaluate: Boolean = false, -) : BaseActor( + val runtimeSymbols: Map = mapOf() +) : BaseActor( prompt = "", name = name, model = model, temperature = temperature, ) { - val fixIterations = 3 - val fixRetries = 2 + val interpreter: Interpreter get() = interpreterClass.java.getConstructor(Map::class.java).newInstance(symbols + runtimeSymbols) + + data class CodeRequest( + val messages: List, + val codePrefix: String = "", + val autoEvaluate: Boolean = false, + val fixIterations: Int = 4, + val fixRetries: Int = 4, + ) + + interface CodeResult { + enum class Status { + Coding, Correcting, Success, Failure + } + + fun getStatus(): Status + fun getCode(): String + fun result(): ExecutionResult + } + + data class ExecutionResult( + val resultValue: String, + val resultOutput: String + ) override val prompt: String get() = if (symbols.isNotEmpty()) """ |You will translate natural language instructions into - |an implementation using ${interpreter.getLanguage()} and the script context. - |Use ``` code blocks labeled with ${interpreter.getLanguage()} where appropriate. - |Defined symbols include ${symbols.keys.joinToString(", ")}. - |The runtime context is described below: + |an implementation using ${language} and the script context. + |Use ``` code blocks labeled with ${language} where appropriate. | + |Defined symbols include {${symbols.keys.joinToString(", ")}} described below: + | + |```${this.describer.markupLanguage} |${this.apiDescription} + |``` | |${details ?: ""} |""".trimMargin().trim() else """ |You will translate natural language instructions into - |an implementation using ${interpreter.getLanguage()} and the script context. - |Use ``` code blocks labeled with ${interpreter.getLanguage()} where appropriate. + |an implementation using ${language} and the script context. + |Use ``` code blocks labeled with ${language} where appropriate. | |${details ?: ""} |""".trimMargin().trim() @@ -67,163 +88,53 @@ open class CodingActor( |""".trimMargin().trim() }.joinToString("\n") - open val interpreter by lazy { interpreterClass.java.getConstructor(Map::class.java).newInstance(symbols) } - - override fun answer(vararg questions: String, api: API): CodeResult = - if (!autoEvaluate) answer(*chatMessages(*questions), api = api) - else answerWithAutoEval(*chatMessages(*questions), api = api).first - - override fun answer(vararg messages: ChatMessage, api: API): CodeResult = - if (!autoEvaluate) CodeResultImpl(*messages, api = (api as OpenAIClient)) - else answerWithAutoEval(*messages, api = api).first - open fun answerWithPrefix( - codePrefix: String, - vararg messages: ChatMessage, - api: API - ): CodeResult = - if (!autoEvaluate) CodeResultImpl(*injectCodePrefix(messages, codePrefix), api = (api as OpenAIClient)) - else answerWithAutoEval(*injectCodePrefix(messages, codePrefix), api = api).first - - open fun answerWithAutoEval( - vararg messages: String, - api: API, - codePrefix: String = "" - ) = answerWithAutoEval(*injectCodePrefix(chatMessages(*messages), codePrefix), api = api) + val language: String by lazy { interpreter.getLanguage() } - open fun answerWithAutoEval( - vararg messages: ChatMessage, - api: API - ): Pair { - var result = CodeResultImpl(*messages, api = (api as OpenAIClient)) - var lastError: Throwable? = null - for (i in 0..fixIterations) try { - return result to result.run() - } catch (ex: Throwable) { - lastError = ex - result = fix(api, messages, result, ex) + override fun chatMessages(questions: CodeRequest): Array { + var chatMessages = arrayOf( + ChatMessage( + role = Role.system, + content = prompt.toContentList() + ), + ) + questions.messages.map { + ChatMessage( + role = Role.user, + content = it.toContentList() + ) } - throw RuntimeException( - """ - |Failed to fix code. Last attempt: - |```${interpreter.getLanguage().lowercase()} - |${result.getCode()} - |``` - | - |Last Error: - |``` - |${lastError?.message} - |``` - |""".trimMargin().trim() - ) - } - - private fun injectCodePrefix( - messages: Array, - codePrefix: String - ) = (messages.dropLast(1) + if (codePrefix.isBlank()) listOf() else listOf( - ChatMessage(Role.assistant, codePrefix.toContentList()) - ) + messages.last()).toTypedArray() + if (questions.codePrefix.isNotBlank()) { + chatMessages = (chatMessages.dropLast(1) + listOf( + ChatMessage(Role.assistant, questions.codePrefix.toContentList()) + ) + chatMessages.last()).toTypedArray() + } + return chatMessages - private fun fix( - api: OpenAIClient, - messages: Array, - result: CodeResultImpl, - ex: Throwable - ): CodeResultImpl { - val respondWithCode = brain(api, model).fixCommand(result.getCode(), ex, "", *messages) - val renderedResponse = getRenderedResponse(respondWithCode.second) - val codedInstruction = getCode(interpreter.getLanguage(), respondWithCode.second) - log.info("Response: \n\t${renderedResponse.replace("\n", "\n\t", false)}".trimMargin()) - log.info("Code: \n\t${codedInstruction.replace("\n", "\n\t", false)}".trimMargin()) - return CodeResultImpl(*messages, codePrefix = codedInstruction, api = api) } - private fun brain(api: OpenAIClient, model: OpenAITextModel) = Brain( - api = api, - symbols = symbols.mapValues { it as Object }.asJava, - language = interpreter.getLanguage(), - describer = describer, - model = model, - temperature = temperature, - ) - - private inner class CodeResultImpl( + override fun answer( vararg messages: ChatMessage, - codePrefix: String = "", - api: OpenAIClient, - ) : CodeResult { - var _status = CodeResult.Status.Coding - override fun getStatus(): CodeResult.Status { - return _status - } - - private val impl by lazy { - var codedInstruction = implement( - this, brain(api, model), messages, codePrefix = codePrefix - ) - if (_status != CodeResult.Status.Success && fallbackModel != model) { - codedInstruction = implement( - this, brain(api, fallbackModel), messages, codePrefix = codePrefix - ) - } - if (_status != CodeResult.Status.Success) { - log.info("Failed to implement ${messages.map { it.content }.joinToString("\n")}") - _status = CodeResult.Status.Failure + input: CodeRequest, + api: API, + ): CodeResult { + var result = CodeResultImpl(*messages, api = (api as OpenAIClient), input = input) + if(!input.autoEvaluate) return result + for (i in 0..input.fixIterations) try { + result.result() + return result + } catch (ex: Throwable) { + if (i == input.fixIterations) { + throw ex } - codedInstruction - } - - @JsonIgnore - override fun getCode(): String = impl - - override fun run() = execute(getCode()) - } - - open fun implement( - self:CodeResult, - brain: Brain, - messages: Array, - codePrefix: String - ): String { - val response = brain.implement(*messages) - val codeBlocks = Brain.extractCodeBlocks(response) - for (codingAttempt in 0..fixRetries) { - var renderedResponse = getRenderedResponse(codeBlocks) - val codedInstruction = getCode(interpreter.getLanguage(), codeBlocks) + val respondWithCode = fixCommand(api, result.getCode(), ex, *messages, model = model) + val codeBlocks = extractCodeBlocks(respondWithCode) + val renderedResponse = getRenderedResponse(codeBlocks) + val codedInstruction = getCode(language, codeBlocks) log.info("Response: \n\t${renderedResponse.replace("\n", "\n\t", false)}".trimMargin()) - log.info("Code: \n\t${codedInstruction.replace("\n", "\n\t", false)}".trimMargin()) - return validateAndFix(self, codedInstruction, codePrefix, brain, messages) ?: continue - } - return "" - } - - open fun validateAndFix( - self : CodeResult, - initialCode: String, - codePrefix: String, - brain: Brain, - messages: Array - ): String? { - var workingCode = initialCode - for (fixAttempt in 0..fixIterations) { - try { - val validate = interpreter.validate((codePrefix + "\n" + workingCode).trim()) - if (validate != null) throw validate - log.info("Validation succeeded") - (self as CodeResultImpl)._status = CodeResult.Status.Success - return workingCode - } catch (ex: Throwable) { - log.info("Validation failed - ${ex.message}") - (self as CodeResultImpl)._status = CodeResult.Status.Correcting - val respondWithCode = brain.fixCommand(workingCode, ex, "", *messages) - val response = getRenderedResponse(respondWithCode.second) - workingCode = getCode(interpreter.getLanguage(), respondWithCode.second) - log.info("Response: \n\t${response.replace("\n", "\n\t", false)}".trimMargin()) - log.info("Code: \n\t${workingCode.replace("\n", "\n\t", false)}".trimMargin()) - } + log.info("New Code: \n\t${codedInstruction.replace("\n", "\n\t", false)}".trimMargin()) + result = CodeResultImpl(*messages, input = input, api = api, givenCode = codedInstruction) } - return null + throw IllegalStateException() } open fun execute(code: String): ExecutionResult { @@ -232,8 +143,10 @@ open class CodingActor( OutputInterceptor.clearGlobalOutput() val result = try { interpreter.run(code) - } catch (ex: ScriptException) { - throw RuntimeException(errorMessage(code, ex.lineNumber, ex.columnNumber, ex.message ?: ""), ex) + } catch (e: Exception) { + if(e is ScriptException) throw FailedToImplementException(e, errorMessage(e, code), code) + if(e.cause is ScriptException) throw FailedToImplementException(e, errorMessage(e.cause!! as ScriptException, code), code) + else throw e } log.info("Result: $result") //language=HTML @@ -242,18 +155,172 @@ open class CodingActor( return executionResult } + private inner class CodeResultImpl( + vararg val messages: ChatMessage, + val input: CodeRequest, + val api: OpenAIClient, + val givenCode: String? = null, + ) : CodeResult { + var _status = CodeResult.Status.Coding + + override fun getStatus() = _status + + private val _code by lazy { + if (null != givenCode) return@lazy givenCode + try { + implement(model) + } catch (ex: FailedToImplementException) { + if (fallbackModel != model) { + try { + implement(fallbackModel) + } catch (ex: FailedToImplementException) { + log.info("Failed to implement ${messages.map { it.content }.joinToString("\n")}") + _status = CodeResult.Status.Failure + throw ex + } + } else { + log.info("Failed to implement ${messages.map { it.content }.joinToString("\n")}") + _status = CodeResult.Status.Failure + throw ex + } + } + } + + private fun implement( + model: ChatModels, + ): String { + val request = ChatRequest(messages = ArrayList(this.messages.toList())) + for (codingAttempt in 0..input.fixRetries) { + try { + val codeBlocks = extractCodeBlocks(chat(api, request, model)) + val renderedResponse = getRenderedResponse(codeBlocks) + val codedInstruction = getCode(language, codeBlocks) + log.info("Response: \n\t${renderedResponse.replace("\n", "\n\t", false)}".trimMargin()) + log.info("New Code: \n\t${codedInstruction.replace("\n", "\n\t", false)}".trimMargin()) + var workingCode = codedInstruction + for (fixAttempt in 0..input.fixIterations) { + try { + val validate = interpreter.validate((input.codePrefix + "\n" + workingCode).sortCode()) + if (validate != null) throw validate + log.info("Validation succeeded") + _status = CodeResult.Status.Success + return workingCode + } catch (ex: Throwable) { + if(fixAttempt == input.fixIterations) throw FailedToImplementException(ex, """ + |Failed to fix code: + | + |```${language.lowercase()} + |${workingCode} + |``` + | + |${ex.message} + """.trimMargin().trim(), workingCode) + log.info("Validation failed - ${ex.message}") + _status = CodeResult.Status.Correcting + val respondWithCode = fixCommand(api, workingCode, ex, *messages, model = model) + val codeBlocks = extractCodeBlocks(respondWithCode) + val response = getRenderedResponse(codeBlocks) + workingCode = getCode(language, codeBlocks) + log.info("Response: \n\t${response.replace("\n", "\n\t", false)}".trimMargin()) + log.info("New Code: \n\t${workingCode.replace("\n", "\n\t", false)}".trimMargin()) + } + } + } catch (ex: FailedToImplementException) { + if (codingAttempt == input.fixRetries) throw ex + log.info("Failed to implement ${messages.map { it.content }.joinToString("\n")}") + _status = CodeResult.Status.Correcting + } + } + throw FailedToImplementException() + } + + @JsonIgnore + override fun getCode(): String = _code + + private val executionResult by lazy { execute((input.codePrefix + "\n" + getCode()).sortCode()) } + override fun result() = executionResult + } + + private fun fixCommand( + api: OpenAIClient, + previousCode: String, + error: Throwable, + vararg promptMessages: ChatMessage, + model: ChatModels + ): String = chat( + api = api, + request = ChatRequest( + messages = ArrayList( + promptMessages.toList() + listOf( + ChatMessage( + Role.assistant, + """ + |```${language.lowercase()} + |${previousCode} + |``` + |""".trimMargin().trim().toContentList() + ), + ChatMessage( + Role.system, + """ + |The previous code failed with the following error: + | + |``` + |${error.message?.trim() ?: ""} + |``` + | + |Correct the code and try again. + |""".trimMargin().trim().toContentList() + ) + ) + ) + ), + model = model + ) + + private fun chat(api: OpenAIClient, request: ChatRequest, model: ChatModels) = + api.chat(request.copy(model = model.modelName, temperature = temperature), model) + .choices.first().message?.content.orEmpty().trim() + companion object { private val log = org.slf4j.LoggerFactory.getLogger(CodingActor::class.java) - fun errorMessage( - code: String, - line: Int, - column: Int, - message: String - ) = """ - |$message at line ${line} column ${column} - | ${code.split("\n")[line - 1]} - | ${" ".repeat(column - 1) + "^"} - """.trimMargin().trim() + + fun String.indent(indent: String = " ") = this.replace("\n", "\n$indent") + + fun extractCodeBlocks(response: String): List> { + val codeBlockRegex = Regex("(?s)```(.*?)\\n(.*?)```") + val languageRegex = Regex("([a-zA-Z0-9-_]+)") + + val result = mutableListOf>() + var startIndex = 0 + + val matches = codeBlockRegex.findAll(response) + if (matches.count() == 0) return listOf(Pair("text", response)) + for (match in matches) { + // Add non-code block before the current match as "text" + if (startIndex < match.range.first) { + result.add(Pair("text", response.substring(startIndex, match.range.first))) + } + + // Extract language and code + val languageMatch = languageRegex.find(match.groupValues[1]) + val language = languageMatch?.groupValues?.get(0) ?: "code" + val code = match.groupValues[2] + + // Add code block to the result + result.add(Pair(language, code)) + + // Update the start index + startIndex = match.range.last + 1 + } + + // Add any remaining non-code text after the last code block as "text" + if (startIndex < response.length) { + result.add(Pair("text", response.substring(startIndex))) + } + + return result + } fun getRenderedResponse(respondWithCode: List>) = respondWithCode.joinToString("\n") { @@ -289,36 +356,76 @@ open class CodingActor( } } - operator fun java.util.Map.plus(mapOf: Map): java.util.Map { - val hashMap = java.util.HashMap() - this.forEach(hashMap::put) - hashMap.putAll(mapOf) - return hashMap as java.util.Map + fun String.sortCode(bodyWrapper: (String) -> String = { it }): String { + val (imports, otherCode) = this.split("\n").partition { it.trim().startsWith("import ") } + return imports.distinct().sorted().joinToString("\n") + "\n\n" + bodyWrapper(otherCode.joinToString("\n")) } - val Map.asJava: java.util.Map - get() { - return java.util.HashMap().also { map -> - this.forEach { (key, value) -> - map[key] = value + fun String.camelCase(locale: Locale = Locale.getDefault()): String { + val words = fromPascalCase().split(" ").map { it.trim() }.filter { it.isNotEmpty() } + return words.first().lowercase(locale) + words.drop(1).joinToString("") { + it.replaceFirstChar { c -> + when { + c.isLowerCase() -> c.titlecase(locale) + else -> c.toString() } - } as java.util.Map + } } + } - } -} + fun String.pascalCase(locale: Locale = Locale.getDefault()): String = + fromPascalCase().split(" ").map { it.trim() }.filter { it.isNotEmpty() }.joinToString("") { + it.replaceFirstChar { c -> + when { + c.isLowerCase() -> c.titlecase(locale) + else -> c.toString() + } + } + } + + // Detect changes in the case of the first letter and prepend a space + fun String.fromPascalCase(): String = buildString { + var lastChar = ' ' + for (c in this@fromPascalCase) { + if (c.isUpperCase() && lastChar.isLowerCase()) append(' ') + append(c) + lastChar = c + } + } + + fun String.upperSnakeCase(locale: Locale = Locale.getDefault()): String = + fromPascalCase().split(" ").map { it.trim() }.filter { it.isNotEmpty() }.joinToString("_") { + it.replaceFirstChar { c -> + when { + c.isLowerCase() -> c.titlecase(locale) + else -> c.toString() + } + } + }.uppercase(locale) + + fun String.imports(): List { + return this.split("\n").filter { it.trim().startsWith("import ") }.distinct().sorted() + } -data class ExecutionResult( - val resultValue: String, - val resultOutput: String -) + fun String.stripImports(): String { + return this.split("\n").filter { !it.trim().startsWith("import ") }.joinToString("\n") + } + + fun errorMessage(ex: ScriptException, code: String) = try { + """ + |${ex.message ?: ""} at line ${ex.lineNumber} column ${ex.columnNumber} + | ${code.split("\n")[ex.lineNumber - 1]} + | ${" ".repeat(ex.columnNumber - 1) + "^"} + """.trimMargin().trim() + } catch (_: Exception) { + ex.message ?: "" + } -interface CodeResult { - enum class Status { - Coding, Correcting, Success, Failure } - fun getStatus(): Status - fun getCode(): String - fun run(): ExecutionResult + class FailedToImplementException( + cause: Throwable? = null, + message: String = "Failed to implement", + val code: String? = null + ) : RuntimeException(message, cause) } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt index 83195376..dcd325b7 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt @@ -2,29 +2,40 @@ package com.simiacryptus.skyenet.core.actors import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel -import com.simiacryptus.jopenai.ApiModel.* +import com.simiacryptus.jopenai.ApiModel.ChatMessage +import com.simiacryptus.jopenai.ApiModel.ImageGenerationRequest +import com.simiacryptus.jopenai.ClientUtil.toContentList import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.ImageModels -import com.simiacryptus.jopenai.models.OpenAITextModel -import com.simiacryptus.jopenai.proxy.ChatProxy import java.awt.image.BufferedImage -import java.util.function.Function open class ImageActor( prompt: String = "Transform the user request into an image generation prompt that the user will like", - val action: String? = null, + name: String? = null, textModel: ChatModels = ChatModels.GPT35Turbo, val imageModel: ImageModels = ImageModels.DallE2, temperature: Double = 0.3, val width: Int = 1024, val height: Int = 1024, -) : BaseActor( +) : BaseActor, ImageResponse>( prompt = prompt, - name = action, + name = name, model = textModel, temperature = temperature, ) { + override fun chatMessages(questions: List) = arrayOf( + ChatMessage( + role = ApiModel.Role.system, + content = prompt.toContentList() + ), + ) + questions.map { + ChatMessage( + role = ApiModel.Role.user, + content = it.toContentList() + ) + } + private inner class ImageResponseImpl(vararg messages: ChatMessage, val api: API) : ImageResponse { private val _text: String by lazy { response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") } @@ -41,7 +52,7 @@ open class ImageActor( } } - override fun answer(vararg messages: ChatMessage, api: API): ImageResponse { + override fun answer(vararg messages: ChatMessage, input: List, api: API): ImageResponse { return ImageResponseImpl(*messages, api = api) } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt index f0a94aa3..45116be0 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt @@ -1,27 +1,39 @@ package com.simiacryptus.skyenet.core.actors import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ClientUtil.toContentList import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.proxy.ChatProxy import java.util.function.Function -open class ParsedActor( +open class ParsedActor( val parserClass: Class>, prompt: String, - val action: String? = null, + name: String? = parserClass.simpleName, model: ChatModels = ChatModels.GPT35Turbo, temperature: Double = 0.3, -) : BaseActor>( +) : BaseActor, ParsedResponse>( prompt = prompt, - name = action, + name = name, model = model, temperature = temperature, ) { val resultClass: Class by lazy { parserClass.getMethod("apply", String::class.java).returnType as Class } + override fun chatMessages(questions: List) = arrayOf( + ApiModel.ChatMessage( + role = ApiModel.Role.system, + content = prompt.toContentList() + ), + ) + questions.map { + ApiModel.ChatMessage( + role = ApiModel.Role.user, + content = it.toContentList() + ) + } - private inner class ParsedResponseImpl(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API) : ParsedResponse(resultClass) { + private inner class ParsedResponseImpl(vararg messages: ApiModel.ChatMessage, api: API) : ParsedResponse(resultClass) { private val parser: Function = ChatProxy( clazz = parserClass, api = (api as OpenAIClient), @@ -34,7 +46,7 @@ open class ParsedActor( override fun getObj(clazz: Class): T = _obj } - override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API): ParsedResponse { + override fun answer(vararg messages: ApiModel.ChatMessage, input: List, api: API): ParsedResponse { return ParsedResponseImpl(*messages, api = api) } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt index 438a3b23..e7fc1e0b 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt @@ -1,22 +1,32 @@ package com.simiacryptus.skyenet.core.actors import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ClientUtil.toContentList import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.OpenAITextModel open class SimpleActor( prompt: String, name: String? = null, model: ChatModels = ChatModels.GPT35Turbo, temperature: Double = 0.3, -) : BaseActor( +) : BaseActor,String>( prompt = prompt, name = name, model = model, temperature = temperature, ) { - override fun answer(vararg questions: String, api: API): String = answer(*chatMessages(*questions), api = api) - - override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API): String = response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") + override fun answer(vararg messages: ApiModel.ChatMessage, input: List, api: API): String = response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") + override fun chatMessages(questions: List) = arrayOf( + ApiModel.ChatMessage( + role = ApiModel.Role.system, + content = prompt.toContentList() + ), + ) + questions.map { + ApiModel.ChatMessage( + role = ApiModel.Role.user, + content = it.toContentList() + ) + } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/opt/ActorOptimization.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/opt/ActorOptimization.kt index da515d3b..daa75558 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/opt/ActorOptimization.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/opt/ActorOptimization.kt @@ -1,5 +1,7 @@ package com.simiacryptus.skyenet.core.actors.opt +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ClientUtil.toContentList import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.describe.Description import com.simiacryptus.jopenai.models.ChatModels @@ -28,15 +30,15 @@ open class ActorOptimization( ) { data class TestCase( - val userMessages: List, + val userMessages: List, val expectations: List, val retries: Int = 3 ) - open fun runGeneticGenerations( + open fun ,T:Any> runGeneticGenerations( prompts: List, testCases: List, - actorFactory: (String) -> BaseActor, + actorFactory: (String) -> BaseActor, resultMapper: (T) -> String, selectionSize: Int = defaultSelectionSize(prompts), populationSize: Int = defaultPositionSize(selectionSize, prompts), @@ -46,7 +48,13 @@ open class ActorOptimization( for (generation in 0..generations) { val scores = topPrompts.map { prompt -> prompt to testCases.map { testCase -> - val answer = actorFactory(prompt).answer(*testCase.userMessages.toTypedArray(), api = api) + val actor = actorFactory(prompt) + val answer = actor.answer(*(listOf( + ApiModel.ChatMessage( + role = ApiModel.Role.system, + content = actor.prompt.toContentList() + ), + ) + testCase.userMessages).toTypedArray(), input = listOf(actor.prompt) as I, api = api) testCase.expectations.map { it.score(api, resultMapper(answer)) }.average() }.average() } @@ -188,6 +196,9 @@ open class ActorOptimization( companion object { private val log = LoggerFactory.getLogger(ActorOptimization::class.java) + fun String.toChatMessage(role: ApiModel.Role = ApiModel.Role.user) = ApiModel.ChatMessage( + role = role, content = this.toContentList() + ) } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/CodingActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/CodingActorInterceptor.kt index 4e9383ce..3cb7f752 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/CodingActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/CodingActorInterceptor.kt @@ -3,8 +3,6 @@ package com.simiacryptus.skyenet.core.actors.record import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel.ChatMessage import com.simiacryptus.jopenai.models.OpenAIModel -import com.simiacryptus.skyenet.core.Brain -import com.simiacryptus.skyenet.core.actors.CodeResult import com.simiacryptus.skyenet.core.actors.CodingActor import com.simiacryptus.skyenet.core.util.FunctionWrapper @@ -20,91 +18,23 @@ class CodingActorInterceptor( model = inner.model, fallbackModel = inner.fallbackModel, temperature = inner.temperature, - autoEvaluate = inner.autoEvaluate, ) { - override fun answer(vararg messages: ChatMessage, api: API) = - functionInterceptor.wrap(messages.toList().toTypedArray()) { - inner.answer(*it, api = api) - } - override fun response( - vararg messages: ChatMessage, + vararg input: ChatMessage, model: OpenAIModel, api: API ) = functionInterceptor.wrap( - messages.toList().toTypedArray(), + input.toList().toTypedArray(), model ) { messages: Array, model: OpenAIModel -> inner.response(*messages, model = model, api = api) } - override fun chatMessages(vararg questions: String) = functionInterceptor.wrap(questions) { - inner.chatMessages(*it) - } - - override fun answer(vararg questions: String, api: API) = functionInterceptor.wrap(questions) { - inner.answer(*it, api = api) - } - override fun answerWithPrefix( - codePrefix: String, - vararg messages: ChatMessage, - api: API - ) = functionInterceptor.wrap( - messages.toList().toTypedArray(), - codePrefix - ) { messages: Array, - codePrefix: String -> - inner.answerWithPrefix(codePrefix, *messages, api = api) - } - - override fun answerWithAutoEval( - vararg messages: String, - api: API, - codePrefix: String - ) = functionInterceptor.wrap( - messages.toList().toTypedArray(), - codePrefix - ) { messages: Array, - codePrefix: String -> - inner.answerWithAutoEval(*messages, api = api, codePrefix = codePrefix) - } - - override fun answerWithAutoEval( - vararg messages: ChatMessage, - api: API - ) = functionInterceptor.wrap(messages.toList().toTypedArray()) { - inner.answerWithAutoEval(*messages, api = api) - } - - override fun implement( - self: CodeResult, - brain: Brain, - messages: Array, - codePrefix: String - ) = functionInterceptor.wrap( - messages.toList().toTypedArray(), - codePrefix - ) { messages: Array, - codePrefix: String -> - inner.implement(self, brain, messages, codePrefix) - } - - override fun validateAndFix( - self: CodeResult, - initialCode: String, - codePrefix: String, - brain: Brain, - messages: Array - ) = functionInterceptor.wrap( - messages.toList().toTypedArray(), - initialCode, - codePrefix - ) { messages: Array, - initialCode: String, - codePrefix: String -> - inner.validateAndFix(self, initialCode, codePrefix, brain, messages) ?: "" + override fun answer(vararg messages: ChatMessage, input: CodeRequest, api: API) = + functionInterceptor.wrap(messages, input) { messages, input -> + inner.answer(*messages, input=input, api = api) } override fun execute(code: String) = functionInterceptor.wrap(code) { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ImageActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ImageActorInterceptor.kt index 67faeb2c..3ff7b6b9 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ImageActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ImageActorInterceptor.kt @@ -3,7 +3,6 @@ package com.simiacryptus.skyenet.core.actors.record import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.models.OpenAIModel import com.simiacryptus.skyenet.core.actors.ImageActor -import com.simiacryptus.skyenet.core.actors.ParsedResponse import com.simiacryptus.skyenet.core.util.FunctionWrapper class ImageActorInterceptor( @@ -11,33 +10,30 @@ class ImageActorInterceptor( private val functionInterceptor: FunctionWrapper, ) : ImageActor( prompt = inner.prompt, - action = inner.action, + name = inner.name, textModel = inner.model, imageModel = inner.imageModel, temperature = inner.temperature, width = inner.width, height = inner.height, ) { - override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API) = + override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, input: List, api: API) = functionInterceptor.wrap(messages.toList().toTypedArray()) { - inner.answer(*it, api = api) + inner.answer(*it, input=input, api = api) } override fun response( - vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, + vararg input: com.simiacryptus.jopenai.ApiModel.ChatMessage, model: OpenAIModel, api: API - ) = functionInterceptor.wrap(messages.toList().toTypedArray(), model) { + ) = functionInterceptor.wrap(input.toList().toTypedArray(), model) { messages: Array, model: OpenAIModel -> inner.response(*messages, model = model, api = api) } - override fun answer(vararg questions: String, api: API) = functionInterceptor.wrap(questions) { - inner.answer(*it, api = api) + override fun answer(input: List, api: API) = functionInterceptor.wrap(input) { + inner.answer(it, api = api) } - override fun chatMessages(vararg questions: String) = functionInterceptor.wrap(questions) { - inner.chatMessages(*it) - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ParsedActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ParsedActorInterceptor.kt index 5209970b..4f72aff6 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ParsedActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/ParsedActorInterceptor.kt @@ -6,36 +6,30 @@ import com.simiacryptus.skyenet.core.actors.ParsedActor import com.simiacryptus.skyenet.core.actors.ParsedResponse import com.simiacryptus.skyenet.core.util.FunctionWrapper -class ParsedActorInterceptor( - val inner: ParsedActor, +class ParsedActorInterceptor( + val inner: ParsedActor<*>, private val functionInterceptor: FunctionWrapper, -) : ParsedActor( - parserClass = inner.parserClass, +) : ParsedActor( + parserClass = inner.parserClass as Class>, prompt = inner.prompt, - action = inner.action, + name = inner.name, model = inner.model, temperature = inner.temperature, ) { - override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API) = + + override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, input: List, api: API) = functionInterceptor.wrap(messages.toList().toTypedArray()) { - inner.answer(*it, api = api) - } + inner.answer(*it, input=input, api = api) + } as ParsedResponse override fun response( - vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, + vararg input: com.simiacryptus.jopenai.ApiModel.ChatMessage, model: OpenAIModel, api: API - ) = functionInterceptor.wrap(messages.toList().toTypedArray(), model) { + ) = functionInterceptor.wrap(input.toList().toTypedArray(), model) { messages: Array, model: OpenAIModel -> inner.response(*messages, model = model, api = api) } - override fun answer(vararg questions: String, api: API) = functionInterceptor.wrap(questions) { - inner.answer(*it, api = api) - } - - override fun chatMessages(vararg questions: String) = functionInterceptor.wrap(questions) { - inner.chatMessages(*it) - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/SimpleActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/SimpleActorInterceptor.kt index dca8af85..121234c0 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/SimpleActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/record/SimpleActorInterceptor.kt @@ -15,27 +15,23 @@ class SimpleActorInterceptor( temperature = inner.temperature, ) { - override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, api: API) = + override fun answer(vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, input: List, api: API) = functionInterceptor.wrap(messages.toList().toTypedArray()) { messages: Array -> - inner.answer(*messages, api = api) + inner.answer(*messages, input=input, api = api) } override fun response( - vararg messages: com.simiacryptus.jopenai.ApiModel.ChatMessage, + vararg input: com.simiacryptus.jopenai.ApiModel.ChatMessage, model: OpenAIModel, api: API - ) = functionInterceptor.wrap(messages.toList().toTypedArray(), model) { + ) = functionInterceptor.wrap(input.toList().toTypedArray(), model) { messages: Array, model: OpenAIModel -> inner.response(*messages, model = model, api = api) } - override fun chatMessages(vararg questions: String) = functionInterceptor.wrap(questions) { - inner.chatMessages(*it) - } - - override fun answer(vararg questions: String, api: API) = functionInterceptor.wrap(questions) { - inner.answer(*it, api = api) + override fun answer(input: List, api: API) = functionInterceptor.wrap(input) { + inner.answer(it, api = api) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ActorTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ActorTestBase.kt index d1182000..c8b2c427 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ActorTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ActorTestBase.kt @@ -1,25 +1,27 @@ package com.simiacryptus.skyenet.core.actors.test +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ClientUtil.toContentList import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.skyenet.core.actors.BaseActor import com.simiacryptus.skyenet.core.actors.opt.ActorOptimization import org.slf4j.LoggerFactory import org.slf4j.event.Level -abstract class ActorTestBase { +abstract class ActorTestBase { open val api = OpenAIClient(logLevel = Level.DEBUG) abstract val testCases: List - abstract val actor: BaseActor - abstract fun actorFactory(prompt: String): BaseActor - abstract fun getPrompt(actor: BaseActor): String + abstract val actor: BaseActor + abstract fun actorFactory(prompt: String): BaseActor + abstract fun getPrompt(actor: BaseActor): String abstract fun resultMapper(result: R): String open fun opt( - actor: BaseActor = this.actor, + actor: BaseActor = this.actor, testCases: List = this.testCases, - actorFactory: (String) -> BaseActor = this::actorFactory, + actorFactory: (String) -> BaseActor = this::actorFactory, resultMapper: (R) -> String = this::resultMapper ) { ActorOptimization( @@ -28,7 +30,7 @@ abstract class ActorTestBase { populationSize = 1, generations = 1, selectionSize = 1, - actorFactory = actorFactory, + actorFactory = actorFactory as (String) -> BaseActor, R>, resultMapper = resultMapper, prompts = listOf( getPrompt(actor), @@ -43,11 +45,20 @@ abstract class ActorTestBase { open fun testRun() { testCases.forEach { testCase -> - val answer = actor.answer(questions = testCase.userMessages.toTypedArray(), api) + val messages = arrayOf( + ApiModel.ChatMessage( + role = com.simiacryptus.jopenai.ApiModel.Role.system, + content = actor.prompt.toContentList() + ), + ) + testCase.userMessages.toTypedArray() + val answer = answer(messages) log.info("Answer: ${resultMapper(answer)}") } } + open fun answer(messages: Array): R = actor.answer(*messages, + input = (messages.map { it.content?.first()?.text }) as I, api=api) + companion object { private val log = LoggerFactory.getLogger(ActorTestBase::class.java) } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/CodingActorTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/CodingActorTestBase.kt index 41c6cdba..5adf0788 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/CodingActorTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/CodingActorTestBase.kt @@ -1,19 +1,19 @@ package com.simiacryptus.skyenet.core.actors.test -import com.simiacryptus.skyenet.core.Heart +import com.simiacryptus.skyenet.core.Interpreter import com.simiacryptus.skyenet.core.actors.BaseActor -import com.simiacryptus.skyenet.core.actors.CodeResult import com.simiacryptus.skyenet.core.actors.CodingActor +import com.simiacryptus.skyenet.core.actors.CodingActor.CodeResult import kotlin.reflect.KClass -abstract class CodingActorTestBase : ActorTestBase() { - abstract val interpreterClass: KClass +abstract class CodingActorTestBase : ActorTestBase() { + abstract val interpreterClass: KClass override fun actorFactory(prompt: String): CodingActor = CodingActor( interpreterClass = interpreterClass, details = prompt, ) - override fun getPrompt(actor: BaseActor): String = (actor as CodingActor).details!! + override fun getPrompt(actor: BaseActor): String = (actor as CodingActor).details!! override fun resultMapper(result: CodeResult): String = result.getCode() } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ImageActorTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ImageActorTestBase.kt index 1660e401..e2db5232 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ImageActorTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ImageActorTestBase.kt @@ -1,12 +1,9 @@ package com.simiacryptus.skyenet.core.actors.test -import com.simiacryptus.skyenet.core.actors.BaseActor import com.simiacryptus.skyenet.core.actors.ImageActor import com.simiacryptus.skyenet.core.actors.ImageResponse -import com.simiacryptus.skyenet.core.actors.ParsedResponse -import java.util.function.Function -abstract class ImageActorTestBase() : ActorTestBase() { +abstract class ImageActorTestBase() : ActorTestBase,ImageResponse>() { override fun actorFactory(prompt: String) = ImageActor( prompt = prompt, ) diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ParsedActorTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ParsedActorTestBase.kt index a6ed1558..a272e2a0 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ParsedActorTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/test/ParsedActorTestBase.kt @@ -7,14 +7,14 @@ import java.util.function.Function abstract class ParsedActorTestBase( private val parserClass: Class>, -) : ActorTestBase>() { +) : ActorTestBase,ParsedResponse>() { override fun actorFactory(prompt: String) = ParsedActor( parserClass = parserClass, prompt = prompt, ) - override fun getPrompt(actor: BaseActor>): String = actor.prompt + override fun getPrompt(actor: BaseActor,ParsedResponse>): String = actor.prompt override fun resultMapper(result: ParsedResponse): String = result.getText() diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthenticationManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthenticationManager.kt index 6ff7b109..7438d33a 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthenticationManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthenticationManager.kt @@ -4,12 +4,13 @@ open class AuthenticationManager { private val users = HashMap() - open fun getUser(sessionId: String?) = if (null == sessionId) null else users[sessionId] + open fun getUser(accessToken: String?) = if (null == accessToken) null else users[accessToken] open fun containsUser(value: String): Boolean = users.containsKey(value) - open fun putUser(sessionId: String, user: User) { - users[sessionId] = user + open fun putUser(accessToken: String, user: User): User { + users[accessToken] = user + return user } companion object { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthorizationManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthorizationManager.kt index 0e5d8b58..e52d9d8d 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthorizationManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/AuthorizationManager.kt @@ -24,7 +24,7 @@ open class AuthorizationManager { } else if (null != applicationClass) { val packagePath = applicationClass.`package`.name.replace('.', '/') val opName = operationType.name.lowercase(Locale.getDefault()) - if (isUserAuthorized("/$packagePath/$opName.txt", user?.email)) { + if (isUserAuthorized("/permissions/$packagePath/$opName.txt", user?.email)) { log.debug("User {} authorized for {} on {}", user, operationType, applicationClass) true } else { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt index 398fdb41..e99a8218 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/ClientManager.kt @@ -5,6 +5,7 @@ import com.simiacryptus.jopenai.ClientUtil import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.models.OpenAIModel +import org.slf4j.LoggerFactory import org.slf4j.event.Level import java.io.File @@ -33,12 +34,19 @@ open class ClientManager { protected open fun createClient( session: Session, user: User?, logfile: File, key: String? = ClientUtil.keyTxt - ): OpenAIClient? = if (key.isNullOrBlank()) null else object : OpenAIClient( + ): OpenAIClient? = if (key.isNullOrBlank()) null else MonitoredClient(key, logfile, session, user) + + inner class MonitoredClient( + key: String, + logfile: File, + private val session: Session, + private val user: User? + ) : OpenAIClient( key = key, logLevel = Level.DEBUG, logStreams = mutableListOf( - logfile.outputStream()?.buffered() - ).filterNotNull().toMutableList(), + logfile.outputStream().buffered() + ), ) { override fun incrementTokens(model: OpenAIModel?, tokens: ApiModel.Usage) { ApplicationServices.usageManager.incrementUsage(session, user, model!!, tokens) @@ -46,5 +54,5 @@ open class ClientManager { } } - private val log = org.slf4j.LoggerFactory.getLogger(ClientManager::class.java) + private val log = LoggerFactory.getLogger(ClientManager::class.java) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/DataStorage.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/DataStorage.kt index 18f52c75..fd2662f2 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/DataStorage.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/DataStorage.kt @@ -29,12 +29,12 @@ open class DataStorage( validateSessionId(session) val messageDir = File(this.getSessionDir(user, session), MESSAGE_DIR) val messages = LinkedHashMap() - log.debug("Loading messages for {}: {}", session, messageDir.absolutePath) + //log.debug("Loading messages for {}: {}", session, messageDir.absolutePath) messageDir.listFiles()?.sortedBy { it.lastModified() }?.forEach { file -> val message = JsonUtil.objectMapper().readValue(file, String::class.java) messages[file.nameWithoutExtension] = message } - log.debug("Loaded {} messages for {}", messages.size, session) + //log.debug("Loaded {} messages for {}", messages.size, session) return messages } @@ -52,17 +52,17 @@ open class DataStorage( else -> throw IllegalArgumentException("Invalid session ID: $session") } val dateDir = File(root, parts[1]) - log.debug("Date Dir for {}: {}", session, dateDir.absolutePath) + //log.debug("Date Dir for {}: {}", session, dateDir.absolutePath) val sessionDir = File(dateDir, parts[2]) - log.debug("Instance Dir for {}: {}", session, sessionDir.absolutePath) + //log.debug("Instance Dir for {}: {}", session, sessionDir.absolutePath) sessionDir } 2 -> { val dateDir = File(dataDir, parts[0]) - log.debug("Date Dir for {}: {}", session, dateDir.absolutePath) + //log.debug("Date Dir for {}: {}", session, dateDir.absolutePath) val sessionDir = File(dateDir, parts[1]) - log.debug("Instance Dir for {}: {}", session, sessionDir.absolutePath) + //log.debug("Instance Dir for {}: {}", session, sessionDir.absolutePath) sessionDir } @@ -79,10 +79,10 @@ open class DataStorage( validateSessionId(session) val userMessage = messages(user, session).entries.minByOrNull { it.key.lastModified() }?.value return if (null != userMessage) { - log.debug("Session {}: {}", session, userMessage) + //log.debug("Session {}: {}", session, userMessage) userMessage } else { - log.debug("Session {}: No messages", session) + //log.debug("Session {}: No messages", session) session.sessionId } } @@ -96,7 +96,7 @@ open class DataStorage( return if (null != file) { Date(file.lastModified()) } else { - log.debug("Session {}: No messages", session) + //log.debug("Session {}: No messages", session) null } } @@ -110,12 +110,12 @@ open class DataStorage( val fileText = messageFile.readText() val split = fileText.split("

") if (split.size < 2) { - log.debug("Session {}: No messages", session) + //log.debug("Session {}: No messages", session) messageFile to "" } else { val stringList = split[1].split("

") if (stringList.isEmpty()) { - log.debug("Session {}: No messages", session) + //log.debug("Session {}: No messages", session) messageFile to "" } else { messageFile to stringList.first() @@ -152,7 +152,7 @@ open class DataStorage( ) { validateSessionId(session) val file = File(File(this.getSessionDir(user, session), MESSAGE_DIR), "$messageId.json") - log.debug("Updating message for {} / {}: {}", session, messageId, file.absolutePath) + //log.debug("Updating message for {} / {}: {}", session, messageId, file.absolutePath) file.parentFile.mkdirs() JsonUtil.objectMapper().writeValue(file, value) } @@ -165,7 +165,7 @@ open class DataStorage( (listFiles?.size ?: 0) > 0 } }?.sortedBy { it.lastModified() } ?: listOf() - log.debug("Sessions: {}", files.map { it.parentFile.name + "-" + it.name }) + //log.debug("Sessions: {}", files.map { it.parentFile.name + "-" + it.name }) return files.map { it.parentFile.name + "-" + it.name } } @@ -189,14 +189,14 @@ open class DataStorage( fun newGlobalID(): Session { val uuid = UUID.randomUUID().toString().split("-").first() val yyyyMMdd = java.time.LocalDate.now().toString().replace("-", "") - log.debug("New ID: $yyyyMMdd-$uuid") + //log.debug("New ID: $yyyyMMdd-$uuid") return Session("G-$yyyyMMdd-$uuid") } fun newUserID(): Session { val uuid = UUID.randomUUID().toString().split("-").first() val yyyyMMdd = java.time.LocalDate.now().toString().replace("-", "") - log.debug("New ID: $yyyyMMdd-$uuid") + //log.debug("New ID: $yyyyMMdd-$uuid") return Session("U-$yyyyMMdd-$uuid") } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/AwsUtil.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/AwsUtil.kt index 67c5505a..bb06cf5c 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/AwsUtil.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/AwsUtil.kt @@ -1,9 +1,10 @@ package com.simiacryptus.skyenet.core.util -import com.amazonaws.services.kms.AWSKMSClientBuilder -import com.amazonaws.services.kms.model.DecryptRequest -import com.amazonaws.services.kms.model.EncryptRequest -import java.nio.ByteBuffer +import software.amazon.awssdk.core.SdkBytes +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.kms.KmsClient +import software.amazon.awssdk.services.kms.model.DecryptRequest +import software.amazon.awssdk.services.kms.model.EncryptRequest import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths @@ -11,16 +12,26 @@ import java.util.* object AwsUtil { + private val kmsClient: KmsClient by lazy { + KmsClient.builder() + .region(Region.US_EAST_1) // Specify the region or use the default region provider chain + .build() + } + fun encryptFile(inputFilePath: String, outputFilePath: String) { val filePath = Paths.get(inputFilePath) val fileBytes = Files.readAllBytes(filePath) - val kmsClient = AWSKMSClientBuilder.standard().build() - val encryptRequest = - EncryptRequest().withKeyId("arn:aws:kms:us-east-1:470240306861:key/a1340b89-64e6-480c-a44c-e7bc0c70dcb1") - .withPlaintext(ByteBuffer.wrap(fileBytes)) + encryptData(fileBytes, outputFilePath) + } + + fun encryptData(fileBytes: ByteArray, outputFilePath: String) { + val encryptRequest = EncryptRequest.builder() + .keyId("arn:aws:kms:us-east-1:470240306861:key/a1340b89-64e6-480c-a44c-e7bc0c70dcb1") + .plaintext(SdkBytes.fromByteArray(fileBytes)) + .build() val result = kmsClient.encrypt(encryptRequest) - val cipherTextBlob = result.ciphertextBlob - val encryptedData = Base64.getEncoder().encodeToString(cipherTextBlob.array()) + val cipherTextBlob = result.ciphertextBlob() + val encryptedData = Base64.getEncoder().encodeToString(cipherTextBlob.asByteArray()) val outputPath = Paths.get(outputFilePath) Files.write(outputPath, encryptedData.toByteArray()) } @@ -31,10 +42,11 @@ object AwsUtil { throw RuntimeException("Unable to load resource: $resourceFile") } val decodedData = Base64.getDecoder().decode(encryptedData) - val kmsClient = AWSKMSClientBuilder.defaultClient() - val decryptRequest = DecryptRequest().withCiphertextBlob(ByteBuffer.wrap(decodedData)) + val decryptRequest = DecryptRequest.builder() + .ciphertextBlob(SdkBytes.fromByteArray(decodedData)) + .build() val decryptResult = kmsClient.decrypt(decryptRequest) - val decryptedData = decryptResult.plaintext.array() + val decryptedData = decryptResult.plaintext().asByteArray() return String(decryptedData, StandardCharsets.UTF_8) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/HeartTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/InterpreterTestBase.kt similarity index 95% rename from core/src/main/kotlin/com/simiacryptus/skyenet/core/util/HeartTestBase.kt rename to core/src/main/kotlin/com/simiacryptus/skyenet/core/util/InterpreterTestBase.kt index 9c5cf44d..c85667d8 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/HeartTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/InterpreterTestBase.kt @@ -1,12 +1,12 @@ package com.simiacryptus.skyenet.core.util -import com.simiacryptus.skyenet.core.Heart +import com.simiacryptus.skyenet.core.Interpreter import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.Map -abstract class HeartTestBase { +abstract class InterpreterTestBase { @Test fun `test run with valid code`() { @@ -87,5 +87,5 @@ abstract class HeartTestBase { assertThrows { with(interpreter.validate("x * y")) { throw this!! } } } - abstract fun newInterpreter(map: Map): Heart + abstract fun newInterpreter(map: Map): Interpreter } \ No newline at end of file diff --git a/core/src/test/java/com/simiacryptus/skyenet/core/actors/ActorOptTest.kt b/core/src/test/java/com/simiacryptus/skyenet/core/actors/ActorOptTest.kt index 81dc424d..822be404 100644 --- a/core/src/test/java/com/simiacryptus/skyenet/core/actors/ActorOptTest.kt +++ b/core/src/test/java/com/simiacryptus/skyenet/core/actors/ActorOptTest.kt @@ -2,6 +2,7 @@ package com.simiacryptus.skyenet.core.actors import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.skyenet.core.actors.opt.ActorOptimization +import com.simiacryptus.skyenet.core.actors.opt.ActorOptimization.Companion.toChatMessage import com.simiacryptus.skyenet.core.actors.opt.Expectation import org.slf4j.LoggerFactory import org.slf4j.event.Level @@ -44,7 +45,7 @@ object ActorOptTest { userMessages = listOf( "I want to buy a book.", "A history book about Napoleon.", - ), + ).map { it.toChatMessage() }, expectations = listOf( Expectation.ContainsMatch("""`search\('.*?'\)`""".toRegex(), critical = false), Expectation.ContainsMatch("""search\(.*?\)""".toRegex(), critical = false), diff --git a/gradle.properties b/gradle.properties index 295bd933..68f40c52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Gradle Releases -> https://github.com/gradle/gradle/releases libraryGroup = com.simiacryptus.skyenet -libraryVersion = 1.0.38 +libraryVersion = 1.0.39 gradleVersion = 7.6.1 # Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library diff --git a/groovy/src/main/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreter.kt b/groovy/src/main/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreter.kt index 1b897667..6e37ae97 100644 --- a/groovy/src/main/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreter.kt +++ b/groovy/src/main/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreter.kt @@ -1,12 +1,12 @@ package com.simiacryptus.skyenet.groovy -import com.simiacryptus.skyenet.core.Heart +import com.simiacryptus.skyenet.core.Interpreter import groovy.lang.GroovyShell import groovy.lang.Script import org.codehaus.groovy.control.CompilationFailedException import org.codehaus.groovy.control.CompilerConfiguration -open class GroovyInterpreter(defs: java.util.Map) : Heart { +open class GroovyInterpreter(val defs: java.util.Map) : Interpreter { private val shell: GroovyShell @@ -22,6 +22,8 @@ open class GroovyInterpreter(defs: java.util.Map) : Heart { return "groovy" } + override fun symbols() = defs as Map + override fun run(code: String): Any? { val wrapExecution = wrapExecution { diff --git a/groovy/src/test/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreterTest.kt b/groovy/src/test/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreterTest.kt index a2e38cdc..f06cee76 100644 --- a/groovy/src/test/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreterTest.kt +++ b/groovy/src/test/kotlin/com/simiacryptus/skyenet/groovy/GroovyInterpreterTest.kt @@ -2,9 +2,9 @@ package com.simiacryptus.skyenet.groovy -import com.simiacryptus.skyenet.core.util.HeartTestBase +import com.simiacryptus.skyenet.core.util.InterpreterTestBase -class GroovyInterpreterTest : HeartTestBase() { +class GroovyInterpreterTest : InterpreterTestBase() { override fun newInterpreter(map: java.util.Map) = GroovyInterpreter(map) } diff --git a/kotlin/src/main/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreter.kt b/kotlin/src/main/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreter.kt index edd94d26..1cb406b2 100644 --- a/kotlin/src/main/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreter.kt +++ b/kotlin/src/main/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreter.kt @@ -2,7 +2,7 @@ package com.simiacryptus.skyenet.kotlin -import com.simiacryptus.skyenet.core.Heart +import com.simiacryptus.skyenet.core.Interpreter import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity @@ -25,6 +25,8 @@ import org.slf4j.LoggerFactory import java.io.File import java.lang.ref.WeakReference import java.lang.reflect.Proxy +import java.net.URL +import java.net.URLClassLoader import java.util.* import java.util.Map import javax.script.Bindings @@ -34,17 +36,15 @@ import kotlin.script.experimental.api.with import kotlin.script.experimental.host.ScriptDefinition import kotlin.script.experimental.jsr223.KOTLIN_JSR223_RESOLVE_FROM_CLASSLOADER_PROPERTY import kotlin.script.experimental.jsr223.KotlinJsr223DefaultScript -import kotlin.script.experimental.jvm.JvmDependencyFromClassLoader -import kotlin.script.experimental.jvm.JvmScriptCompilationConfigurationBuilder -import kotlin.script.experimental.jvm.jvm -import kotlin.script.experimental.jvm.updateClasspath +import kotlin.script.experimental.jvm.* import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContext import kotlin.script.experimental.jvmhost.createJvmScriptDefinitionFromTemplate import kotlin.script.experimental.jvmhost.jsr223.KotlinJsr223ScriptEngineImpl open class KotlinInterpreter( private val defs: Map = HashMap() as Map -) : Heart { +) : Interpreter { + override fun symbols() = defs as kotlin.collections.Map override fun validate(code: String): Throwable? { val messageCollector = MessageCollectorImpl(code) @@ -124,33 +124,18 @@ open class KotlinInterpreter( protected open fun jvmCompilerArguments(code: String): K2JVMCompilerArguments { val arguments = K2JVMCompilerArguments() - //arguments.fragmentSources = arrayOf(tempFile.absolutePath) -// arguments.allowNoSourceFiles = false arguments.expression = code arguments.classpath = System.getProperty("java.class.path") -// arguments.compileJava = true -// arguments.allowAnyScriptsInSourceRoots = true -// arguments.allowUnstableDependencies = false -// arguments.checkPhaseConditions = true arguments.enableDebugMode = true -// arguments.enableSignatureClashChecks = true arguments.extendedCompilerChecks = true -// arguments.linkViaSignatures = true arguments.reportOutputFiles = true arguments.moduleName = "KotlinInterpreter" arguments.noOptimize = true -// arguments.noReflect = true arguments.script = true arguments.validateIr = true arguments.validateBytecode = true arguments.verbose = true -// arguments.javaParameters = true arguments.useTypeTable = true -// arguments.useJavac = true -// arguments.useFirExtendedCheckers = true -// arguments.destination = "kotlinBuild" -// File(arguments.destination).mkdirs() - return arguments } @@ -174,6 +159,7 @@ open class KotlinInterpreter( updateClasspath(classPath) } + protected open val scriptEngineFactory by lazy { KotlinScriptEngineFactory() } inner class KotlinScriptEngineFactory : KotlinJsr223JvmScriptEngineFactoryBase() { @@ -188,7 +174,11 @@ open class KotlinInterpreter( } } }, - scriptDefinition.evaluationConfiguration + scriptDefinition.evaluationConfiguration.with { + jvm { + set(baseClassLoader, Thread.currentThread().contextClassLoader.isolatedClassLoader()) + } + } ) { ScriptArgsWithTypes( arrayOf(it.getBindings(ScriptContext.ENGINE_SCOPE).orEmpty()), @@ -218,7 +208,7 @@ open class KotlinInterpreter( } } throw RuntimeException( - errorMessage(code, lineNumber, column, ex.message ?: ""), ex + errorMessage(wrappedCode, lineNumber, column, ex.message ?: ""), ex ) } } @@ -261,6 +251,8 @@ open class KotlinInterpreter( | ${code.split("\n")[line - 1]} | ${" ".repeat(column - 1) + "^"} """.trimMargin().trim() + + fun ClassLoader.isolatedClassLoader() = URLClassLoader(arrayOf(), this) } override fun getLanguage(): String { diff --git a/kotlin/src/test/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreterTest.kt b/kotlin/src/test/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreterTest.kt index 8272e748..8c268559 100644 --- a/kotlin/src/test/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreterTest.kt +++ b/kotlin/src/test/kotlin/com/simiacryptus/skyenet/kotlin/KotlinInterpreterTest.kt @@ -2,12 +2,12 @@ package com.simiacryptus.skyenet.kotlin -import com.simiacryptus.skyenet.core.util.HeartTestBase +import com.simiacryptus.skyenet.core.util.InterpreterTestBase import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.util.Map -class KotlinInterpreterTest : HeartTestBase() { +class KotlinInterpreterTest : InterpreterTestBase() { override fun newInterpreter(map: Map) = KotlinInterpreter(map) diff --git a/scala/src/main/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreter.scala b/scala/src/main/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreter.scala index 535e47a7..ead5140d 100644 --- a/scala/src/main/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreter.scala +++ b/scala/src/main/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreter.scala @@ -1,11 +1,12 @@ package com.simiacryptus.skyenet.scala -import com.simiacryptus.skyenet.core.Heart +import com.simiacryptus.skyenet.core.Interpreter import com.simiacryptus.skyenet.scala.ScalaLocalInterpreter.log import java.nio.file.Paths +import java.util import java.util.function.Supplier -import scala.jdk.CollectionConverters.MapHasAsScala +import scala.jdk.CollectionConverters.{MapHasAsJava, MapHasAsScala} import scala.reflect.internal.util.Position import scala.reflect.runtime.universe._ import scala.tools.nsc.Settings @@ -22,7 +23,7 @@ object ScalaLocalInterpreter { } -class ScalaLocalInterpreter(javaDefs: java.util.Map[String, Object]) extends Heart { +class ScalaLocalInterpreter(javaDefs: java.util.Map[String, Object]) extends Interpreter { val defs: Map[String, Any] = javaDefs.asInstanceOf[java.util.Map[String, Any]].asScala.toMap val typeTags: Map[String, Type] = javaDefs.asScala.map(x => (x._1, ScalaLocalInterpreter.getTypeTag(x._2))).toMap @@ -148,4 +149,8 @@ class ScalaLocalInterpreter(javaDefs: java.util.Map[String, Object]) extends Hea override def wrapExecution[T](fn: Supplier[T]): T = fn.get() + override def symbols(): util.Map[String, AnyRef] = defs.map { t => + (t._1, t._2.asInstanceOf[AnyRef]) + }.asJava + } \ No newline at end of file diff --git a/scala/src/test/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreterTest.scala b/scala/src/test/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreterTest.scala index 443afb6c..1e6c72da 100644 --- a/scala/src/test/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreterTest.scala +++ b/scala/src/test/scala/com/simiacryptus/skyenet/scala/ScalaLocalInterpreterTest.scala @@ -1,13 +1,13 @@ package com.simiacryptus.skyenet.scala -import com.simiacryptus.skyenet.core.Heart -import com.simiacryptus.skyenet.core.util.HeartTestBase +import com.simiacryptus.skyenet.core.Interpreter +import com.simiacryptus.skyenet.core.util.InterpreterTestBase import java.util -class ScalaLocalInterpreterTest extends HeartTestBase { - override def newInterpreter(map: util.Map[String, AnyRef]): Heart = { +class ScalaLocalInterpreterTest extends InterpreterTestBase { + override def newInterpreter(map: util.Map[String, AnyRef]): Interpreter = { new ScalaLocalInterpreter(map) } } diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index e4573dab..d46a9a51 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -32,7 +32,7 @@ val jetty_version = "11.0.18" val jackson_version = "2.15.3" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.35") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.36") implementation(project(":core")) testImplementation(project(":groovy")) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationDirectory.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationDirectory.kt index 882d2284..8fd57a9c 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationDirectory.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationDirectory.kt @@ -14,6 +14,8 @@ import org.eclipse.jetty.server.handler.ContextHandlerCollection import org.eclipse.jetty.servlet.FilterHolder import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.util.resource.Resource.newResource +import org.eclipse.jetty.util.resource.ResourceCollection import org.eclipse.jetty.webapp.WebAppContext import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer import org.slf4j.LoggerFactory @@ -39,14 +41,14 @@ abstract class ApplicationDirectory( private fun domainName(isServer: Boolean) = if (isServer) "https://$publicName" else "http://$localName:$port" - open val welcomeResources = Resource.newResource(javaClass.classLoader.getResource("welcome")) - ?: throw IllegalStateException("No welcome resource") + + open val welcomeResources = ResourceCollection(allResources("welcome").map(::newResource)) open val userInfoServlet = UserInfoServlet() open val userSettingsServlet = UserSettingsServlet() open val usageServlet = UsageServlet() open val proxyHttpServlet = ProxyHttpServlet() open val welcomeServlet = WelcomeServlet(this) - open fun authenticatedWebsite(): AuthenticatedWebsite? = AuthenticatedWebsite( + open fun authenticatedWebsite(): OAuthBase? = OAuthGoogle( redirectUri = "$domainName/oauth2callback", applicationName = "Demo", key = { decryptResource("client_secret_google_oauth.json.kms").byteInputStream() } @@ -153,6 +155,8 @@ abstract class ApplicationDirectory( companion object { private val log = LoggerFactory.getLogger(com.simiacryptus.skyenet.webui.application.ApplicationDirectory::class.java) + fun allResources(resourceName: String) = + Thread.currentThread().contextClassLoader.getResources(resourceName).toList() } } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt index f2c47d0a..bc90d38b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationInterface.kt @@ -1,21 +1,20 @@ package com.simiacryptus.skyenet.webui.application -import com.simiacryptus.skyenet.webui.session.SessionMessage -import com.simiacryptus.skyenet.webui.session.SocketManagerBase +import com.simiacryptus.jopenai.describe.Description +import com.simiacryptus.skyenet.webui.session.SessionTask import java.util.function.Consumer class ApplicationInterface(private val inner: ApplicationSocketManager) { - fun send(html: String) = inner.send(html) + @Description("Returns html for a link that will trigger the given handler when clicked.") fun hrefLink(linkText: String, classname: String = """href-link""", handler: Consumer) = inner.hrefLink(linkText, classname, handler) + @Description("Returns html for a text input form that will trigger the given handler when submitted.") fun textInput(handler: Consumer): String = inner.textInput(handler) - fun newMessage( - operationID: String = SocketManagerBase.randomID(), - spinner: String = SessionMessage.spinner, - cancelable: Boolean = false - ): SessionMessage = inner.newMessage(operationID, spinner, cancelable) + fun newTask( + //cancelable: Boolean = false + ): SessionTask = inner.newTask(false) } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt index 8d6fb34b..757e824a 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationServer.kt @@ -11,7 +11,6 @@ import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.User import com.simiacryptus.skyenet.webui.chat.ChatServer import com.simiacryptus.skyenet.webui.servlet.* -import com.simiacryptus.skyenet.webui.session.SessionMessage import com.simiacryptus.skyenet.webui.session.SocketManager import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationSocketManager.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationSocketManager.kt index 6bf3ca28..a69c48a8 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationSocketManager.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/application/ApplicationSocketManager.kt @@ -6,7 +6,7 @@ import com.simiacryptus.skyenet.core.platform.DataStorage import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.User import com.simiacryptus.skyenet.webui.chat.ChatSocket -import com.simiacryptus.skyenet.webui.session.SessionMessage +import com.simiacryptus.skyenet.webui.session.SessionTask import com.simiacryptus.skyenet.webui.session.SocketManagerBase import java.util.function.Consumer @@ -80,7 +80,7 @@ abstract class ApplicationSocketManager( ) companion object { - val spinner: String get() = """
${SessionMessage.spinner}
""" + val spinner: String get() = """
${SessionTask.spinner}
""" // val playButton: String get() = """""" // val cancelButton: String get() = """""" // val regenButton: String get() = """""" diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocket.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocket.kt index dca9ba38..f0678cff 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocket.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocket.kt @@ -11,7 +11,7 @@ class ChatSocket( override fun onWebSocketConnect(session: Session) { super.onWebSocketConnect(session) - log.debug("{} - Socket connected: {}", session, session.remote) + //log.debug("{} - Socket connected: {}", session, session.remote) sessionState.addSocket(this) sessionState.getReplay().forEach { try { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt index 3a5c2da0..1a86ba3a 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/chat/ChatSocketManager.kt @@ -7,7 +7,7 @@ import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.webui.application.ApplicationServer -import com.simiacryptus.skyenet.webui.session.SessionMessage +import com.simiacryptus.skyenet.webui.session.SessionTask import com.simiacryptus.skyenet.webui.session.SocketManagerBase import com.simiacryptus.skyenet.webui.util.MarkdownUtil @@ -42,7 +42,7 @@ open class ChatSocketManager( override fun onRun(userMessage: String, socket: ChatSocket) { var responseContents = divInitializer(cancelable = false) responseContents += """
${renderResponse(userMessage)}
""" - send("""$responseContents
${SessionMessage.spinner}
""") + send("""$responseContents
${SessionTask.spinner}
""") messages += ApiModel.ChatMessage(ApiModel.Role.user, userMessage.toContentList()) try { val response = api.chat( diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthBase.kt new file mode 100644 index 00000000..49a6af65 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthBase.kt @@ -0,0 +1,7 @@ +package com.simiacryptus.skyenet.webui.servlet + +import org.eclipse.jetty.webapp.WebAppContext + +abstract class OAuthBase(val redirectUri: String) { + abstract fun configure(context: WebAppContext, addFilter: Boolean = true): WebAppContext +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/AuthenticatedWebsite.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthGoogle.kt similarity index 58% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/AuthenticatedWebsite.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthGoogle.kt index b808c70e..e258198a 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/AuthenticatedWebsite.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/OAuthGoogle.kt @@ -6,11 +6,9 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport import com.google.api.client.json.gson.GsonFactory import com.google.api.services.oauth2.Oauth2 -import com.google.api.services.oauth2.model.Userinfo import com.simiacryptus.skyenet.core.platform.ApplicationServices import com.simiacryptus.skyenet.core.platform.AuthenticationManager.Companion.AUTH_COOKIE import com.simiacryptus.skyenet.core.platform.User -import com.simiacryptus.skyenet.webui.application.ApplicationServer.Companion.getCookie import jakarta.servlet.* import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServlet @@ -29,51 +27,38 @@ import java.util.* import java.util.concurrent.TimeUnit -open class AuthenticatedWebsite( - val redirectUri: String, +open class OAuthGoogle( + redirectUri: String, val applicationName: String, key: () -> InputStream? -) { - - open fun newUserSession(userInfo: Userinfo, sessionId: String) { - log.info("User $userInfo logged in with session $sessionId") - ApplicationServices.authenticationManager.putUser(sessionId, User( - id = userInfo.id, - email = userInfo.email, - name = userInfo.name, - picture = userInfo.picture - ) - ) - } - - open fun configure(context: WebAppContext, addFilter: Boolean = true): WebAppContext { - context.addServlet(ServletHolder("googleLogin", GoogleLoginServlet()), "/googleLogin") - context.addServlet(ServletHolder("oauth2callback", OAuth2CallbackServlet()), "/oauth2callback") - if (addFilter) context.addFilter(FilterHolder(SessionIdFilter()), "/*", EnumSet.of(DispatcherType.REQUEST)) +) : OAuthBase(redirectUri) { + + override fun configure(context: WebAppContext, addFilter: Boolean): WebAppContext { + context.addServlet(ServletHolder("googleLogin", LoginServlet()), "/login") + context.addServlet(ServletHolder("googleLogin", LoginServlet()), "/googleLogin") + context.addServlet(ServletHolder("oauth2callback", CallbackServlet()), "/oauth2callback") + if (addFilter) context.addFilter(FilterHolder(SessionIdFilter({ request -> + setOf("/googleLogin", "/oauth2callback").none { request.requestURI.startsWith(it) } + }, "/googleLogin")), "/*", EnumSet.of(DispatcherType.REQUEST)) return context } - open fun isSecure(request: HttpServletRequest) = - setOf("/googleLogin", "/oauth2callback").none { request.requestURI.startsWith(it) } - private val httpTransport = GoogleNetHttpTransport.newTrustedTransport() private val jsonFactory = GsonFactory.getDefaultInstance() - private val clientSecrets: GoogleClientSecrets = GoogleClientSecrets.load( - jsonFactory, - InputStreamReader(key()!!) - ) - private val flow = GoogleAuthorizationCodeFlow.Builder( httpTransport, jsonFactory, - clientSecrets, + GoogleClientSecrets.load( + jsonFactory, + InputStreamReader(key()!!) + ), listOf( "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile" ) ).build() - private inner class GoogleLoginServlet : HttpServlet() { + private inner class LoginServlet : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { val redirect = req.getParameter("redirect") ?: "" val state = URLEncoder.encode(redirect, StandardCharsets.UTF_8.toString()) @@ -92,37 +77,24 @@ open class AuthenticatedWebsite( } } - private inner class SessionIdFilter : Filter { - - override fun init(filterConfig: FilterConfig?) {} - - override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { - if (request is HttpServletRequest && response is HttpServletResponse) { - if (isSecure(request)) { - val sessionIdCookie = request.getCookie() - if (sessionIdCookie == null || !ApplicationServices.authenticationManager.containsUser(sessionIdCookie)) { - response.sendRedirect("/googleLogin") - return - } - } - } - chain.doFilter(request, response) - } - - override fun destroy() {} - } - - private inner class OAuth2CallbackServlet : HttpServlet() { + private inner class CallbackServlet : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { val code = req.getParameter("code") if (code != null) { - val tokenResponse = flow.newTokenRequest(code).setRedirectUri(redirectUri).execute() - val credential = flow.createAndStoreCredential(tokenResponse, null) - val oauth2 = - Oauth2.Builder(httpTransport, jsonFactory, credential).setApplicationName(applicationName).build() - val userInfo: Userinfo = oauth2.userinfo().get().execute() val sessionID = UUID.randomUUID().toString() - newUserSession(userInfo, sessionID) + val userInfo = Oauth2.Builder( + httpTransport, jsonFactory, flow.createAndStoreCredential( + flow.newTokenRequest(code).setRedirectUri(redirectUri).execute(), null + ) + ).setApplicationName(applicationName).build().userinfo().get().execute() + val user = User( + id = userInfo.id, + email = userInfo.email, + name = userInfo.name, + picture = userInfo.picture + ) + ApplicationServices.authenticationManager.putUser(accessToken = sessionID, user = user) + log.info("User $user logged in with session $sessionID") val sessionCookie = Cookie(AUTH_COOKIE, sessionID) sessionCookie.path = "/" sessionCookie.isHttpOnly = true @@ -139,7 +111,7 @@ open class AuthenticatedWebsite( } companion object { - private val log = org.slf4j.LoggerFactory.getLogger(AuthenticatedWebsite::class.java) + private val log = org.slf4j.LoggerFactory.getLogger(OAuthGoogle::class.java) fun String.urlDecode(): String? = try { URLDecoder.decode(this, StandardCharsets.UTF_8.toString()) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionIdFilter.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionIdFilter.kt new file mode 100644 index 00000000..b82d010c --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionIdFilter.kt @@ -0,0 +1,33 @@ +package com.simiacryptus.skyenet.webui.servlet + +import com.simiacryptus.skyenet.core.platform.ApplicationServices +import com.simiacryptus.skyenet.webui.application.ApplicationServer.Companion.getCookie +import jakarta.servlet.* +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +class SessionIdFilter( + val isSecure: (HttpServletRequest) -> Boolean, + val loginRedirect: String +) : Filter { + + override fun init(filterConfig: FilterConfig?) {} + + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + if (request is HttpServletRequest && response is HttpServletResponse) { + if (isSecure(request)) { + val sessionIdCookie = request.getCookie() + if (sessionIdCookie == null || !ApplicationServices.authenticationManager.containsUser( + sessionIdCookie + ) + ) { + response.sendRedirect(loginRedirect) + return + } + } + } + chain.doFilter(request, response) + } + + override fun destroy() {} +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt index 5bc16507..c403770d 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/SessionListServlet.kt @@ -1,6 +1,6 @@ package com.simiacryptus.skyenet.webui.servlet -import com.simiacryptus.skyenet.core.Brain.Companion.indent +import com.simiacryptus.skyenet.core.actors.CodingActor.Companion.indent import com.simiacryptus.skyenet.core.platform.ApplicationServices.authenticationManager import com.simiacryptus.skyenet.core.platform.DataStorage import com.simiacryptus.skyenet.webui.application.ApplicationServer.Companion.getCookie diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/WelcomeServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/WelcomeServlet.kt index ad6f29fa..8fd9f2a1 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/WelcomeServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/servlet/WelcomeServlet.kt @@ -70,7 +70,7 @@ open class WelcomeServlet(private val parent : com.simiacryptus.skyenet.webui.ap
- Login + Login
diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionMessage.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt similarity index 94% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionMessage.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt index cf67186e..2605388e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionMessage.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SessionTask.kt @@ -5,9 +5,9 @@ import org.slf4j.LoggerFactory import java.awt.image.BufferedImage import java.util.* -abstract class SessionMessage( +abstract class SessionTask( private var buffer: MutableList = mutableListOf(), - private val spinner: String = SessionMessage.spinner + private val spinner: String = SessionTask.spinner ) { val currentText: String get() = buffer.filter { it.isNotBlank() }.joinToString("") @@ -62,7 +62,7 @@ abstract class SessionMessage( add("""""") companion object { - val log = LoggerFactory.getLogger(SessionMessage::class.java) + val log = LoggerFactory.getLogger(SessionTask::class.java) const val spinner = """
Loading...
""" diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt index ae993b3e..b590cc42 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt @@ -6,10 +6,8 @@ import com.simiacryptus.skyenet.webui.chat.ChatServer import com.simiacryptus.skyenet.webui.chat.ChatSocket import com.simiacryptus.skyenet.webui.util.MarkdownUtil import org.slf4j.LoggerFactory -import java.net.URL import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger -import kotlin.io.path.Path abstract class SocketManagerBase( protected val session: Session, @@ -45,20 +43,18 @@ abstract class SocketManagerBase( } } - fun newMessage( - operationID: String = randomID(), - spinner: String = SessionMessage.spinner, + fun newTask( cancelable: Boolean = false - ): SessionMessage { - var responseContents = divInitializer(operationID, cancelable) + ): SessionTask { + var responseContents = divInitializer(randomID(), cancelable) send(responseContents) - return SessionMessageImpl(responseContents, spinner) + return SessionTaskImpl(responseContents, SessionTask.spinner) } - inner class SessionMessageImpl( + inner class SessionTaskImpl( responseContents: String, - spinner: String = SessionMessage.spinner - ) : SessionMessage(mutableListOf(StringBuilder(responseContents)), spinner) { + spinner: String = SessionTask.spinner + ) : SessionTask(mutableListOf(StringBuilder(responseContents)), spinner) { override fun send(html: String) = this@SocketManagerBase.send(html) override fun save(file: String, data: ByteArray): String { dataStorage?.getSessionDir(user, session)?.let { dir -> diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt index ac627f16..f8fba397 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/CodingActorTestApp.kt @@ -14,7 +14,7 @@ import java.util.* open class CodingActorTestApp( private val actor: CodingActor, - applicationName: String = "CodingActorTest_" + actor.interpreter.javaClass.simpleName, + applicationName: String = "CodingActorTest_" + actor.name, temperature: Double = 0.3, ) : ApplicationServer( applicationName = applicationName, @@ -27,10 +27,10 @@ open class CodingActorTestApp( ui: ApplicationInterface, api: API ) { - val message = ui.newMessage() + val message = ui.newTask() try { message.echo(renderMarkdown(userMessage)) - val response = actor.answer(userMessage, api = api) + val response = actor.answer(CodingActor.CodeRequest(listOf(userMessage)), api = api) val canPlay = ApplicationServices.authorizationManager.isAuthorized( this::class.java, user, @@ -39,7 +39,7 @@ open class CodingActorTestApp( val playLink = if (!canPlay) "" else { ui.hrefLink("â–¶", "href-link play-button") { message.add("Running...") - val result = response.run() + val result = response.result() message.complete( """ |
${result.resultValue}
@@ -51,7 +51,7 @@ open class CodingActorTestApp( message.complete( renderMarkdown( """ - |```${actor.interpreter.getLanguage().lowercase(Locale.getDefault())} + |```${actor.language.lowercase(Locale.getDefault())} |${response.getCode()} |``` |$playLink diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ImageActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ImageActorTestApp.kt index ffdeb2eb..faecf7bd 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ImageActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ImageActorTestApp.kt @@ -31,11 +31,12 @@ open class ImageActorTestApp( ui: ApplicationInterface, api: API ) { - val message = ui.newMessage() + val message = ui.newTask() try { val actor = getSettings(session, user)?.actor ?: actor message.echo(renderMarkdown(userMessage)) - val response = actor.answer(userMessage, api = api) + val response = actor.answer( + listOf(userMessage), api = api) message.verbose(response.getText()) message.image(response.getImage()) message.complete() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ParsedActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ParsedActorTestApp.kt index 1d0501df..9b52e3e4 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ParsedActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/ParsedActorTestApp.kt @@ -25,10 +25,10 @@ open class ParsedActorTestApp( ui: ApplicationInterface, api: API ) { - val message = ui.newMessage() + val message = ui.newTask() try { message.echo(renderMarkdown(userMessage)) - val response = actor.answer(userMessage, api = api) + val response = actor.answer(listOf(userMessage), api = api) message.complete( renderMarkdown( """ diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/SimpleActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/SimpleActorTestApp.kt index 03df505b..180ebfdc 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/SimpleActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/test/SimpleActorTestApp.kt @@ -31,11 +31,11 @@ open class SimpleActorTestApp( ui: ApplicationInterface, api: API ) { - val message = ui.newMessage() + val message = ui.newTask() try { val actor = getSettings(session, user)?.actor ?: actor message.echo(renderMarkdown(userMessage)) - val response = actor.answer(userMessage, api = api) + val response = actor.answer(listOf(userMessage), api = api) message.complete(renderMarkdown(response)) } catch (e: Throwable) { log.warn("Error", e) diff --git a/webui/src/main/resources/application/index.html b/webui/src/main/resources/application/index.html index 107a000e..e5920d71 100644 --- a/webui/src/main/resources/application/index.html +++ b/webui/src/main/resources/application/index.html @@ -32,7 +32,7 @@
- Login + Login
diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index 1ef3f0c6..a0fc4e30 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -201,7 +201,7 @@ document.addEventListener('DOMContentLoaded', () => { const loginLink = document.getElementById('username'); if (loginLink) { - loginLink.href = '/googleLogin?redirect=' + encodeURIComponent(window.location.pathname); + loginLink.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); } fetch('appInfo') diff --git a/webui/src/main/resources/welcome/favicon.png b/webui/src/main/resources/welcome/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c9880c39d00890614f689ff9b119b36e3bb7a9a1 GIT binary patch literal 57618 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 Ju}r}{ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webui/src/main/resources/welcome/main.js b/webui/src/main/resources/welcome/main.js index 34898a29..4668bcd1 100644 --- a/webui/src/main/resources/welcome/main.js +++ b/webui/src/main/resources/welcome/main.js @@ -29,7 +29,7 @@ document.addEventListener('DOMContentLoaded', () => { const loginLink = document.getElementById('username'); if (loginLink) { - loginLink.href = '/googleLogin?redirect=' + encodeURIComponent(window.location.pathname); + loginLink.href = '/login?redirect=' + encodeURIComponent(window.location.pathname); } fetch('userInfo') diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt index bd9f972b..15fea872 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt @@ -47,9 +47,9 @@ object ActorTestAppServer : com.simiacryptus.skyenet.webui.application.Applicati "" ) ApplicationServices.authenticationManager = object : AuthenticationManager() { - override fun getUser(sessionId: String?) = mockUser + override fun getUser(accessToken: String?) = mockUser override fun containsUser(value: String) = true - override fun putUser(sessionId: String, user: User) = throw UnsupportedOperationException() + override fun putUser(accessToken: String, user: User) = throw UnsupportedOperationException() } ApplicationServices.authorizationManager = object : AuthorizationManager() { override fun isAuthorized(