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 00000000..c9880c39 Binary files /dev/null and b/webui/src/main/resources/welcome/favicon.png differ diff --git a/webui/src/main/resources/welcome/favicon.svg b/webui/src/main/resources/welcome/favicon.svg index 6a09be9f..1777362f 100644 --- a/webui/src/main/resources/welcome/favicon.svg +++ b/webui/src/main/resources/welcome/favicon.svg @@ -1,724 +1,679 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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(