From 6c66815882643742cf2141475d49ce1bc90cb0ec Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 12 Nov 2023 01:26:16 -0500 Subject: [PATCH] 1.0.24 (#28) * 1.0.24 * 1.0.24 * wip * wip * wip * user settings * Token counting * moves * Improved readonly views * misc * welcome page updates --- README.md | 6 +- core/build.gradle.kts | 2 +- .../kotlin/com/simiacryptus/skyenet/Ears.kt | 4 +- .../kotlin/com/simiacryptus/skyenet/Heart.kt | 2 +- .../kotlin/com/simiacryptus/skyenet/Mouth.kt | 4 +- .../simiacryptus/skyenet/actors/BaseActor.kt | 8 +- .../skyenet/actors/CodingActor.kt | 13 +- .../skyenet/actors/ParsedActor.kt | 22 +- .../skyenet/actors/SimpleActor.kt | 7 +- .../skyenet/{ => util}/HeartTestBase.kt | 3 +- .../skyenet/util/SessionServerUtil.kt | 2 +- gradle.properties | 2 +- .../skyenet/GroovyInterpreterTest.kt | 5 +- .../skyenet/heart/KotlinInterpreter.kt | 2 +- .../skyenet/KotlinInterpreterTest.kt | 6 +- .../heart/ScalaLocalInterpreterTest.scala | 3 +- webui/build.gradle.kts | 2 +- .../skyenet/body/BasicChatSession.kt | 27 - .../skyenet/body/ChatSessionFlexmark.kt | 48 -- .../skyenet/body/InterviewSession.kt | 71 -- .../skyenet/body/MacroChatSession.kt | 77 -- .../skyenet/body/OperationStatus.kt | 129 ---- .../skyenet/body/SessionDataStorage.kt | 139 ---- .../skyenet/body/SessionInterface.kt | 8 - .../skyenet/body/SkyenetBasicChat.kt | 27 - .../skyenet/body/SkyenetCodingSession.kt | 236 ------ .../body/SkyenetCodingSessionServer.kt | 73 -- .../skyenet/body/SkyenetInterviewer.kt | 66 -- .../skyenet/body/SkyenetMacroChat.kt | 73 -- .../skyenet/body/SkyenetMarkupChat.kt | 77 -- .../skyenet/body/SkyenetSessionServerBase.kt | 203 ----- .../skyenet/body/WebSocketServer.kt | 130 ---- .../skyenet/servers/AppServerBase.kt | 156 +++- .../skyenet/servers/CodingActorTestApp.kt | 23 +- .../skyenet/servers/EmbeddingVisualizer.kt | 2 +- .../skyenet/servers/ParsedActorTestApp.kt | 19 +- .../skyenet/servers/ReadOnlyApp.kt | 15 +- .../skyenet/servers/SessionServerUtil.kt | 40 - .../skyenet/servers/SimpleActorTestApp.kt | 27 +- .../{body => servlet}/AuthenticatedWebsite.kt | 291 +++---- .../skyenet/servlet/FileServlet.kt | 58 ++ .../skyenet/servlet/NewSessionServlet.kt | 15 + .../{servers => servlet}/ProxyHttpServlet.kt | 2 +- .../skyenet/servlet/SessionServlet.kt | 33 + .../skyenet/servlet/SessionSettingsServlet.kt | 57 ++ .../skyenet/servlet/UsageServlet.kt | 107 +++ .../skyenet/servlet/UserInfoServlet.kt | 19 + .../skyenet/servlet/UserSettingsServlet.kt | 75 ++ .../skyenet/servlet/ZipServlet.kt | 45 ++ .../skyenet/sessions/ApplicationBase.kt | 105 +++ .../sessions/AsyncApplicationWrapper.kt | 55 ++ .../skyenet/sessions/ChatApplicationBase.kt | 32 + .../skyenet/{body => sessions}/ChatSession.kt | 28 +- .../skyenet/sessions/MessageWebSocket.kt | 72 ++ .../MutableSessionHandler.kt | 62 +- .../PersistentSessionBase.kt | 173 ++--- .../skyenet/{body => sessions}/SessionBase.kt | 116 +-- .../skyenet/sessions/SessionDataStorage.kt | 161 ++++ .../skyenet/sessions/SessionDiv.kt | 7 + .../skyenet/sessions/SessionInterface.kt | 8 + .../skyenet/sessions/WebSocketServer.kt | 53 ++ .../{body => util}/ClasspathResource.kt | 230 +++--- .../simiacryptus/skyenet/util/HtmlTools.kt | 29 + .../simiacryptus/skyenet/util/MarkdownUtil.kt | 21 + webui/src/main/resources/readOnly/chat.css | 228 ++++++ webui/src/main/resources/readOnly/chat.js | 65 ++ webui/src/main/resources/readOnly/favicon.svg | 724 ++++++++++++++++++ webui/src/main/resources/readOnly/index.html | 45 ++ webui/src/main/resources/readOnly/main.js | 130 ++++ .../src/main/resources/simpleSession/chat.css | 22 + .../main/resources/simpleSession/favicon.png | Bin 126746 -> 0 bytes .../main/resources/simpleSession/favicon.svg | 724 ++++++++++++++++++ .../main/resources/simpleSession/index.html | 17 +- .../src/main/resources/simpleSession/main.js | 77 +- webui/src/main/resources/welcome/chat.css | 236 +----- webui/src/main/resources/welcome/favicon.png | Bin 126746 -> 0 bytes webui/src/main/resources/welcome/favicon.svg | 724 ++++++++++++++++++ webui/src/main/resources/welcome/main.js | 56 ++ .../skyenet/ActorTestAppServer.kt | 2 +- .../com/simiacryptus/skyenet/BasicChatTest.kt | 28 - .../com/simiacryptus/skyenet/DemoAgent.kt | 47 -- .../simiacryptus/skyenet/FlexmarkChatTest.kt | 28 - .../{body => }/SessionDataStorageTest.kt | 95 +-- 83 files changed, 4434 insertions(+), 2427 deletions(-) rename core/src/main/kotlin/com/simiacryptus/skyenet/{ => util}/HeartTestBase.kt (97%) delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/BasicChatSession.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSessionFlexmark.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/InterviewSession.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/MacroChatSession.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/OperationStatus.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionDataStorage.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionInterface.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetBasicChat.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSession.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSessionServer.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetInterviewer.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMacroChat.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMarkupChat.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetSessionServerBase.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/body/WebSocketServer.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SessionServerUtil.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/{body => servlet}/AuthenticatedWebsite.kt (63%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/{servers => servlet}/ProxyHttpServlet.kt (98%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ApplicationBase.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/AsyncApplicationWrapper.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatApplicationBase.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/{body => sessions}/ChatSession.kt (69%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MessageWebSocket.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/{body => sessions}/MutableSessionHandler.kt (68%) rename webui/src/main/kotlin/com/simiacryptus/skyenet/{body => sessions}/PersistentSessionBase.kt (73%) rename webui/src/main/kotlin/com/simiacryptus/skyenet/{body => sessions}/SessionBase.kt (82%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDiv.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionInterface.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/WebSocketServer.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/{body => util}/ClasspathResource.kt (95%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt create mode 100644 webui/src/main/resources/readOnly/chat.css create mode 100644 webui/src/main/resources/readOnly/chat.js create mode 100644 webui/src/main/resources/readOnly/favicon.svg create mode 100644 webui/src/main/resources/readOnly/index.html create mode 100644 webui/src/main/resources/readOnly/main.js delete mode 100644 webui/src/main/resources/simpleSession/favicon.png create mode 100644 webui/src/main/resources/simpleSession/favicon.svg delete mode 100644 webui/src/main/resources/welcome/favicon.png create mode 100644 webui/src/main/resources/welcome/favicon.svg create mode 100644 webui/src/main/resources/welcome/main.js delete mode 100644 webui/src/test/kotlin/com/simiacryptus/skyenet/BasicChatTest.kt delete mode 100644 webui/src/test/kotlin/com/simiacryptus/skyenet/DemoAgent.kt delete mode 100644 webui/src/test/kotlin/com/simiacryptus/skyenet/FlexmarkChatTest.kt rename webui/src/test/kotlin/com/simiacryptus/skyenet/{body => }/SessionDataStorageTest.kt (62%) diff --git a/README.md b/README.md index 2588b432..11f71762 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.23 + 1.0.24 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.23' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.24' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.23") +implementation("com.simiacryptus:skyenet:1.0.24") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ea0b24a9..a9071613 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -33,7 +33,7 @@ val logback_version = "1.2.12" dependencies { - implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.25") + implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.26") implementation(group = "org.slf4j", name = "slf4j-api", version = "2.0.9") implementation(group = "commons-io", name = "commons-io", version = "2.11.0") diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/Ears.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/Ears.kt index 9f39be62..cbd90bb7 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/Ears.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/Ears.kt @@ -14,10 +14,10 @@ import java.util.concurrent.atomic.AtomicInteger /** * The ears are the interface to the audio input for the SkyeNet system */ -@Suppress("MemberVisibilityCanBePrivate", "unused") +@Suppress("unused") open class Ears( val api: OpenAIClient, - val secondsPerAudioPacket : Double = 0.25, + private val secondsPerAudioPacket : Double = 0.25, ) { interface CommandRecognizer { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/Heart.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/Heart.kt index ea97d030..21713d03 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/Heart.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/Heart.kt @@ -10,8 +10,8 @@ interface Heart { fun wrapExecution(fn: java.util.function.Supplier): T? = fn.get() companion object { - @Suppress("unused") private class TestObject { + @Suppress("unused") fun square(x: Int): Int = x * x } private interface TestInterface { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/Mouth.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/Mouth.kt index 85cd9a81..4218ea99 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/Mouth.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/Mouth.kt @@ -15,9 +15,9 @@ import javax.sound.sampled.SourceDataLine /** * The mouth is the interface to the Google Text-to-Speech API for the SkyeNet system */ -@Suppress("MemberVisibilityCanBePrivate", "unused") +@Suppress("unused") open class Mouth( - val keyfile: String + private val keyfile: String ) { open fun speak(text: String) { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/BaseActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/BaseActor.kt index b7525d3a..e0e99b27 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/BaseActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/BaseActor.kt @@ -5,12 +5,10 @@ import com.simiacryptus.openai.OpenAIClient abstract class BaseActor( open val prompt: String, val name: String? = null, - val api: OpenAIClient = OpenAIClient(), val model: OpenAIClient.Models = OpenAIClient.Models.GPT35Turbo, val temperature: Double = 0.3, ) { - - open fun response(vararg messages: OpenAIClient.ChatMessage, model: OpenAIClient.Models = this.model) = api.chat( + open fun response(vararg messages: OpenAIClient.ChatMessage, model: OpenAIClient.Models = this.model, api: OpenAIClient) = api.chat( OpenAIClient.ChatRequest( messages = messages.toList().toTypedArray(), temperature = temperature, @@ -18,8 +16,8 @@ abstract class BaseActor( ), model = this.model ) - abstract fun answer(vararg messages: OpenAIClient.ChatMessage): T - open fun answer(vararg questions: String): T = answer(*chatMessages(*questions)) + abstract fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): T + open fun answer(vararg questions: String, api: OpenAIClient): T = answer(*chatMessages(*questions), api = api) open fun chatMessages(vararg questions: String) = arrayOf( OpenAIClient.ChatMessage( diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt index cd8240c1..3fbda314 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt @@ -22,14 +22,12 @@ class CodingActor( ), name: String? = interpreterClass.simpleName, val details: String? = null, - api: OpenAIClient = OpenAIClient(), model: OpenAIClient.Models = OpenAIClient.Models.GPT35Turbo, val fallbackModel: OpenAIClient.Models = OpenAIClient.Models.GPT4Turbo, temperature: Double = 0.1, ) : BaseActor( prompt = "", name = name, - api = api, model = model, temperature = temperature, ) { @@ -89,22 +87,23 @@ class CodingActor( open val interpreter by lazy { interpreterClass.java.getConstructor(Map::class.java).newInstance(symbols) } - override fun answer(vararg questions: String): CodeResult = answer(*chatMessages(*questions)) + override fun answer(vararg questions: String, api: OpenAIClient): CodeResult = answer(*chatMessages(*questions), api = api) - override fun answer(vararg messages: OpenAIClient.ChatMessage): CodeResult { - return CodeResultImpl(*messages) + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): CodeResult { + return CodeResultImpl(*messages, api = api) } - fun answerWithPrefix(codePrefix: String, vararg messages: OpenAIClient.ChatMessage): CodeResult { + fun answerWithPrefix(codePrefix: String, vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): CodeResult { val prevList = messages.toList() val newList = prevList.dropLast(1) + listOf( OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.assistant, codePrefix) ) + prevList.last() - return CodeResultImpl(*newList.toTypedArray()) + return CodeResultImpl(*newList.toTypedArray(), api = api) } private inner class CodeResultImpl( vararg messages: OpenAIClient.ChatMessage, codePrefix: String = "", + api: OpenAIClient, ) : CodeResult { var _status = CodeResult.Status.Coding override fun getStatus(): CodeResult.Status { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ParsedActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ParsedActor.kt index 66d795b1..b0717a4d 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ParsedActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ParsedActor.kt @@ -8,31 +8,29 @@ class ParsedActor( val parserClass: Class>, prompt: String, val action: String? = null, - api: OpenAIClient = OpenAIClient(), model: OpenAIClient.Models = OpenAIClient.Models.GPT35Turbo, temperature: Double = 0.3, ) : BaseActor>( - api = api, prompt = prompt, name = action, model = model, temperature = temperature, ) { - private val parser: Function = ChatProxy( - clazz = parserClass, - api = api, - model = OpenAIClient.Models.GPT35Turbo, - temperature = temperature, - ).create() - private inner class ParsedResponseImpl(vararg messages: OpenAIClient.ChatMessage) : ParsedResponse { - private val _text: String by lazy { response(*messages).choices.first().message?.content ?: throw RuntimeException("No response") } + private inner class ParsedResponseImpl(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient) : ParsedResponse { + val parser: Function = ChatProxy( + clazz = parserClass, + api = api, + model = OpenAIClient.Models.GPT35Turbo, + temperature = temperature, + ).create() + private val _text: String by lazy { response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") } private val _obj: T by lazy { parser.apply(getText()) } override fun getText(): String = _text override fun getObj(): T = _obj } - override fun answer(vararg messages: OpenAIClient.ChatMessage): ParsedResponse { - return ParsedResponseImpl(*messages) + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): ParsedResponse { + return ParsedResponseImpl(*messages, api = api) } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/SimpleActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/SimpleActor.kt index 0415dc18..2582d144 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/SimpleActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/SimpleActor.kt @@ -5,20 +5,17 @@ import com.simiacryptus.openai.OpenAIClient class SimpleActor( prompt: String, name: String? = null, - api: OpenAIClient = OpenAIClient(), model: OpenAIClient.Models = OpenAIClient.Models.GPT35Turbo, temperature: Double = 0.3, ) : BaseActor( prompt = prompt, name = name, - api = api, model = model, temperature = temperature, - ) { - override fun answer(vararg questions: String): String = answer(*chatMessages(*questions)) + override fun answer(vararg questions: String, api: OpenAIClient): String = answer(*chatMessages(*questions), api = api) - override fun answer(vararg messages: OpenAIClient.ChatMessage): String = response(*messages).choices.first().message?.content ?: throw RuntimeException("No response") + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): String = response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/HeartTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/util/HeartTestBase.kt similarity index 97% rename from core/src/main/kotlin/com/simiacryptus/skyenet/HeartTestBase.kt rename to core/src/main/kotlin/com/simiacryptus/skyenet/util/HeartTestBase.kt index d7f8d787..a0a4209c 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/HeartTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/util/HeartTestBase.kt @@ -1,5 +1,6 @@ -package com.simiacryptus.skyenet +package com.simiacryptus.skyenet.util +import com.simiacryptus.skyenet.Heart import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt index 3a0e82b1..e9d4d5e2 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt @@ -24,7 +24,7 @@ object SessionServerUtil { } } - val logger = org.slf4j.LoggerFactory.getLogger(SessionServerUtil::class.java) + val log = org.slf4j.LoggerFactory.getLogger(SessionServerUtil::class.java) fun getCode(language: String, textSegments: List>) = textSegments.joinToString("\n") { diff --git a/gradle.properties b/gradle.properties index 0199a4cd..cb87d5b5 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.23 +libraryVersion = 1.0.24 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/test/kotlin/com/simiacryptus/skyenet/GroovyInterpreterTest.kt b/groovy/src/test/kotlin/com/simiacryptus/skyenet/GroovyInterpreterTest.kt index 4f9ece54..e394f18f 100644 --- a/groovy/src/test/kotlin/com/simiacryptus/skyenet/GroovyInterpreterTest.kt +++ b/groovy/src/test/kotlin/com/simiacryptus/skyenet/GroovyInterpreterTest.kt @@ -1,8 +1,9 @@ +@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + package com.simiacryptus.skyenet.heart.test -import com.simiacryptus.skyenet.HeartTestBase +import com.simiacryptus.skyenet.util.HeartTestBase import com.simiacryptus.skyenet.heart.GroovyInterpreter -import org.junit.jupiter.api.Test class GroovyInterpreterTest : HeartTestBase() { override fun newInterpreter(map: java.util.Map) = GroovyInterpreter(map) diff --git a/kotlin/src/main/kotlin/com/simiacryptus/skyenet/heart/KotlinInterpreter.kt b/kotlin/src/main/kotlin/com/simiacryptus/skyenet/heart/KotlinInterpreter.kt index 6cbf5c0b..7d16cd6a 100644 --- a/kotlin/src/main/kotlin/com/simiacryptus/skyenet/heart/KotlinInterpreter.kt +++ b/kotlin/src/main/kotlin/com/simiacryptus/skyenet/heart/KotlinInterpreter.kt @@ -156,7 +156,7 @@ open class KotlinInterpreter( } } - val scriptEngineFactory = object : KotlinJsr223JvmScriptEngineFactoryBase() { + private val scriptEngineFactory = object : KotlinJsr223JvmScriptEngineFactoryBase() { override fun getScriptEngine(): ScriptEngine { return KotlinJsr223ScriptEngineImpl( this, diff --git a/kotlin/src/test/kotlin/com/simiacryptus/skyenet/KotlinInterpreterTest.kt b/kotlin/src/test/kotlin/com/simiacryptus/skyenet/KotlinInterpreterTest.kt index e21a6f02..4f44a3cf 100644 --- a/kotlin/src/test/kotlin/com/simiacryptus/skyenet/KotlinInterpreterTest.kt +++ b/kotlin/src/test/kotlin/com/simiacryptus/skyenet/KotlinInterpreterTest.kt @@ -1,13 +1,15 @@ +@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + package com.simiacryptus.skyenet.heart -import com.simiacryptus.skyenet.HeartTestBase +import com.simiacryptus.skyenet.util.HeartTestBase import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.util.Map class KotlinInterpreterTest : HeartTestBase() { - override fun newInterpreter(map: java.util.Map) = KotlinInterpreter(map) + override fun newInterpreter(map: Map) = KotlinInterpreter(map) @Test override fun `test run with variables`() { diff --git a/scala/src/test/scala/com/simiacryptus/skyenet/heart/ScalaLocalInterpreterTest.scala b/scala/src/test/scala/com/simiacryptus/skyenet/heart/ScalaLocalInterpreterTest.scala index c97589f3..f425c2a1 100644 --- a/scala/src/test/scala/com/simiacryptus/skyenet/heart/ScalaLocalInterpreterTest.scala +++ b/scala/src/test/scala/com/simiacryptus/skyenet/heart/ScalaLocalInterpreterTest.scala @@ -1,7 +1,8 @@ package com.test import com.simiacryptus.skyenet.heart.ScalaLocalInterpreter -import com.simiacryptus.skyenet.{Heart, HeartTestBase} +import com.simiacryptus.skyenet.Heart +import com.simiacryptus.skyenet.util.HeartTestBase import java.util diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index 17a94139..79c3c9b2 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -32,7 +32,7 @@ val jetty_version = "11.0.17" val jackson_version = "2.15.2" dependencies { - implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.25") + implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.26") implementation(project(":core")) testImplementation(project(":groovy")) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/BasicChatSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/BasicChatSession.kt deleted file mode 100644 index e890039d..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/BasicChatSession.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient - -open class BasicChatSession( - parent: SkyenetSessionServerBase, - model: OpenAIClient.Model = OpenAIClient.Models.GPT35Turbo, - sessionId: String, - visiblePrompt: String = """ - |Hello! I am here to assist you in a casual conversation! - |Feel free to ask me anything or just chat about your day. - """.trimMargin(), - hiddenPrompt: String = """ - |I understand that the user might want to have a casual conversation. - |So, I'll respond in a friendly and engaging manner. - |I will also ask questions to keep the conversation going. - |Once we have finished our conversation, I'll say goodbye. - | - |${visiblePrompt} - """.trimMargin(), - systemPrompt: String = """ - |You are a friendly and conversational AI that engages in casual chat with users. - |Your task is to respond to the user's messages in a friendly and engaging manner. - |Ask questions to keep the conversation going. - |Say goodbye when the conversation is over. - """.trimMargin(), -) : ChatSession(parent, sessionId, model, visiblePrompt, hiddenPrompt, systemPrompt) \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSessionFlexmark.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSessionFlexmark.kt deleted file mode 100644 index 8ef854ea..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSessionFlexmark.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.vladsch.flexmark.ext.tables.TablesExtension -import com.vladsch.flexmark.html.HtmlRenderer -import com.vladsch.flexmark.parser.Parser -import com.vladsch.flexmark.util.data.MutableDataSet - -open class ChatSessionFlexmark( - parent: SkyenetSessionServerBase, - model: OpenAIClient.Model = OpenAIClient.Models.GPT35Turbo, - sessionId: String, - visiblePrompt: String, - hiddenPrompt: String, - systemPrompt: String, -) : ChatSession( - parent, - sessionId, - model, - visiblePrompt, - hiddenPrompt, - systemPrompt -) { - - override fun renderResponse(response: String): String { - return renderMarkdown(response, flexmarkOptions()) - } - - open fun flexmarkOptions(): MutableDataSet { - return defaultOptions() - } - - companion object { - fun renderMarkdown(response: String, options: MutableDataSet = defaultOptions()): String { - val parser = Parser.builder(options).build() - val renderer = HtmlRenderer.builder(options).build() - val document = parser.parse(response) - return renderer.render(document) - } - - fun defaultOptions(): MutableDataSet { - val options = MutableDataSet() - options.set(Parser.EXTENSIONS, listOf(TablesExtension.create())) - return options - } - } - -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/InterviewSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/InterviewSession.kt deleted file mode 100644 index 2f87d071..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/InterviewSession.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.openai.OpenAIClient.ChatMessage -import com.simiacryptus.util.JsonUtil -import com.simiacryptus.util.describe.TypeDescriber -import com.simiacryptus.util.describe.YamlDescriber - -open class InterviewSession( - parent: SkyenetSessionServerBase, - model: OpenAIClient.Model = OpenAIClient.Models.GPT35Turbo, - sessionId: String, - val dataClass: Class, - describer: TypeDescriber = YamlDescriber(), - visiblePrompt: String = """ - |Hello! I am here to assist you in specifying a ${dataClass.simpleName}! - |I will guide you through a series of questions to gather the necessary information. - |Don't worry if you're not sure about any technical details; I'm here to help! - """.trimMargin(), - val isFinished: (T) -> List, - hiddenPrompt: String = """ - |I understand that the user might not know the technical details of the data structure we need to fill. - |So, I'll ask questions in a way that's easy for a layperson to understand and provide clarifications if needed. - |At the end of each message, I will output the currently-accumulated JSON of the data using ```json``` blocks. - |Whenever asking a question, I will provide some suggestions. - |I will also ask for clarification when needed. - |Once we have gathered all the necessary information, I'll provide you with the final JSON. - | - |${visiblePrompt} - | - |The current data structure is: - |```json - |${JsonUtil.toJson(dataClass.getDeclaredConstructor().newInstance())} - |``` - """.trimMargin(), - systemPrompt: String = """ - |You are a friendly and conversational AI that helps interview users to assist in preparing a request data structure. - |The data structure is defined as follows: - |```yaml - |${describer.describe(dataClass)} - |``` - |The user will provide information, but it may not be in the correct format or they might not know the exact fields to fill. - |Your task is to ask questions and guide the user to infer and fill the correct data structure. - |Provide a list of suggestions whenever asking a question. - |Ask for clarification when needed. - |At the end of each assistant chat message, print the currently-accumulated JSON of the data using ```json``` blocks. - """.trimMargin(), -) : ChatSession(parent, sessionId, model, visiblePrompt, hiddenPrompt, systemPrompt) { - - open fun onFinished(data: T) {} - open fun onUpdate(data: T) {} - override fun onResponse(response: String, responseContents: String) { - - // If the response contains a JSON (```json``` block), parse the data structure and check if it's finished - val jsonRegex = Regex("""(?s)```json\s*(?.*?)\s*```""") - val jsonMatch = jsonRegex.find(response) - if (jsonMatch != null) { - val json = jsonMatch.groups["json"]!!.value - val data = JsonUtil.fromJson(json, dataClass) - val validationResult = isFinished(data) - if (validationResult.isEmpty()) { - send(responseContents + """
Finished!
""") - onFinished(data) - } else { - messages += ChatMessage(ChatMessage.Role.system, "Validation errors: ${validationResult.joinToString(", ")}") - onUpdate(data) - } - } - } - -} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/MacroChatSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/MacroChatSession.kt deleted file mode 100644 index 9a068bb6..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/MacroChatSession.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.simiacryptus.skyenet.body - -import java.util.concurrent.Semaphore -import java.util.function.Consumer - -class MacroChatSession( - val parent : SkyenetMacroChat, - sessionId: String, - sessionDataStorage: SessionDataStorage = parent.sessionDataStorage -) : PersistentSessionBase( - sessionId = sessionId, - sessionDataStorage = sessionDataStorage -) { - private val playSempaphores = mutableMapOf() - private val threads = mutableMapOf() - private val regenTriggers = mutableMapOf>() - val linkTriggers = mutableMapOf>() - val txtTriggers = mutableMapOf>() - val session : PersistentSessionBase = this - override fun run(userMessage: String) { - val operationID = ChatSession.randomID() - val sessionDiv = newSessionDiv(operationID, SkyenetSessionServerBase.spinner) - val thread = Thread { - playSempaphores[operationID] = Semaphore(0) - try { - parent.processMessage(sessionId, userMessage, session, object : - SkyenetMacroChat.SessionUI { - override val spinner: String get() = """
${SkyenetSessionServerBase.spinner}
""" - override val playButton: String get() = """""" - override val cancelButton: String get() = """""" - override val regenButton: String get() = """""" - - override fun hrefLink(handler: Consumer): String { - val operationID = ChatSession.randomID() - linkTriggers[operationID] = handler - return """""" - } - - override fun textInput(handler: Consumer): String { - val operationID = ChatSession.randomID() - txtTriggers[operationID] = handler - //language=HTML - return """
- - -
""".trimIndent() - } - - }, sessionDiv) - } catch (e: Throwable) { - e.printStackTrace() - } finally { - } - } - threads[operationID] = thread - thread.start() - } - - override fun onCmd(id: String, code: String) { - if(code=="run") { - playSempaphores[id]?.release() - } - if(code=="cancel") { - threads[id]?.interrupt() - } - if(code=="regen") { - regenTriggers[id]?.accept(Unit) - } - if(code.startsWith("link")) { - linkTriggers[id]?.accept(Unit) - } - if(code.startsWith("userTxt,")) { - txtTriggers[id]?.accept(code.substring("userTxt,".length)) - } - super.onCmd(id, code) - } -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/OperationStatus.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/OperationStatus.kt deleted file mode 100644 index b50529a0..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/OperationStatus.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonProperty -import com.simiacryptus.openai.OpenAIClient.ChatMessage - - -class OperationStatus @JsonCreator constructor( - @JsonProperty("created") val created: Long = System.currentTimeMillis(), - @JsonProperty("language") val language: String = "", - @JsonProperty("operationID") val operationID: String = "", - @JsonProperty("instruction") val instruction: String = "", - @JsonProperty("status") var status: OperationState = OperationState.Pending, - @JsonProperty("responseText") var responseText: String = "", - @JsonProperty("responseCode") var responseCode: String = "", - @JsonProperty("resultValue") var resultValue: String = "", - @JsonProperty("resultOutput") var resultOutput: String = "", - @JsonIgnore val thread: Thread? = null, -) { - enum class OperationState { - Pending, Implemented, Running, Complete, Cancelled, Error - } - - companion object { - val logger = org.slf4j.LoggerFactory.getLogger(SkyenetCodingSessionServer::class.java) - } - fun onMessage(code: String) { - if (code.lowercase() == "run") { - runSemaphore.release() - logger.debug("$operationID - Running") - } else if (code.lowercase() == "stop") { - cancelFlag.set(true) - thread?.interrupt() - logger.debug("$operationID - Stopping") - } else { - logger.warn("$operationID - Unknown command: $code") - } - } - - @JsonIgnore val runSemaphore = java.util.concurrent.Semaphore(0) - @JsonIgnore val cancelFlag = java.util.concurrent.atomic.AtomicBoolean(false) - val chatMessages - @JsonIgnore get() = if (status == OperationState.Error) { - listOf( - ChatMessage( - ChatMessage.Role.user, """ - |${truncate(instruction)} - """.trimMargin() - ), ChatMessage( - ChatMessage.Role.assistant, """ - |```${language} - |${truncate(responseCode)} - |``` - |""".trimMargin().trim() - ), - ChatMessage( - ChatMessage.Role.system, """ - |Error: - |``` - |${truncate(resultValue)} - |``` - """.trimMargin().trim() - ) - ) - } else { - listOf( - ChatMessage( - ChatMessage.Role.user, """ - |${truncate(instruction)} - """.trimMargin() - ), ChatMessage( - ChatMessage.Role.assistant, """ - |```${language} - |${truncate(responseCode)} - |``` - |""".trimMargin().trim() - ), - ChatMessage( - ChatMessage.Role.system, """ - |Output: - |``` - |${truncate(resultOutput)} - |``` - | - |Returns: - |``` - |${truncate(resultValue)} - |``` - """.trimMargin().trim() - ) - ) - } - private fun truncate(text: String, length: Int = 500): String { - return if (text.length > length) text.substring(0, length - 3) + "..." else text - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as OperationStatus - - if (created != other.created) return false - if (language != other.language) return false - if (operationID != other.operationID) return false - if (instruction != other.instruction) return false - if (status != other.status) return false - if (responseText != other.responseText) return false - if (responseCode != other.responseCode) return false - if (resultValue != other.resultValue) return false - return resultOutput == other.resultOutput - } - - override fun hashCode(): Int { - var result = created.hashCode() - result = 31 * result + language.hashCode() - result = 31 * result + operationID.hashCode() - result = 31 * result + instruction.hashCode() - result = 31 * result + status.hashCode() - result = 31 * result + responseText.hashCode() - result = 31 * result + responseCode.hashCode() - result = 31 * result + resultValue.hashCode() - result = 31 * result + resultOutput.hashCode() - return result - } - - -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionDataStorage.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionDataStorage.kt deleted file mode 100644 index 57b234d2..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionDataStorage.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.util.JsonUtil -import java.io.File - -open class SessionDataStorage( - val dataDir: File = File("sessionData") -) { - - init { - if (!dataDir.exists()) { - dataDir.mkdirs() - } - } - - open fun updateOperationStatus(sessionId: String, operationId: String, value: OperationStatus) { - val operationDir = getOperationDir(sessionId) - val operationFile = File(operationDir, "$operationId.json") - JsonUtil.objectMapper().writeValue(operationFile, value) - } - - open fun updateMessage(sessionId: String, messageId: String, value: String) { - val messageDir = getMessageDir(sessionId) - val messageFile = File(messageDir, "$messageId.json") - - JsonUtil.objectMapper().writeValue(messageFile, value) - } - - open fun loadOperations(sessionId: String): MutableMap { - val operationDir = getOperationDir(sessionId) - val operations = mutableMapOf() - - operationDir.listFiles()?.forEach { file -> - val operation = JsonUtil.objectMapper().readValue(file, OperationStatus::class.java) - operations[file.nameWithoutExtension] = operation - } - - return operations - } - - open fun loadMessages(sessionId: String): LinkedHashMap { - val messageDir = getMessageDir(sessionId) - val messages = LinkedHashMap() - - messageDir.listFiles()?.forEach { file -> - val message = JsonUtil.objectMapper().readValue(file, String::class.java) - messages[file.nameWithoutExtension] = message - } - - return messages - } - - protected open fun getOperationDir(sessionId: String): File { - val sessionDir = getSessionInstanceDir(sessionId) - val operationDir = File(sessionDir, "operations") - if (!operationDir.exists()) { - operationDir.mkdirs() - } - return operationDir - } - - protected open fun getMessageDir(sessionId: String): File { - val sessionDir = getSessionInstanceDir(sessionId) - val messageDir = File(sessionDir, "messages") - if (!messageDir.exists()) { - messageDir.mkdirs() - } - return messageDir - } - - open fun listSessions(): List { - // For all sessions, return the session id - // Filter out sessions which have no operations - val files = dataDir.listFiles()?.flatMap { it.listFiles().toList() }?.filter { sessionDir -> - val operationDir = File(sessionDir, "messages") - if (!operationDir.exists()) false else { - val listFiles = operationDir.listFiles() - (listFiles?.size ?: 0) > 2 - } - } - return files?.map { it.parentFile.name + "-" + it.name } ?: listOf() - } - - open fun getSessionName(sessionId: String): String { - val sessionDir = getSessionInstanceDir(sessionId) - // Find the earliest operation and return the operation instruction - val operationDir = File(sessionDir, "operations") - val operationFiles = operationDir.listFiles()?.filter { file -> - file.isFile - }?.sortedBy { file -> - JsonUtil.objectMapper().readValue(file, OperationStatus::class.java).created - } ?: listOf() - if (operationFiles.isNotEmpty()) { - val operationFile = operationFiles.first() - val operationStatus = JsonUtil.objectMapper().readValue(operationFile, OperationStatus::class.java) - return operationStatus.instruction - } else { - return sessionId - } - } - - protected open fun getSessionInstanceDir(sessionId: String): File { - val sessionDir = File(getSessionGroupDir(sessionId), getSessionInstanceId(sessionId)) - if (!sessionDir.exists()) { - sessionDir.mkdirs() - } - return sessionDir - } - - open fun getSessionDir(sessionId: String) = getSessionGroupDir(sessionId) - - protected open fun getSessionGroupDir(sessionId: String): File { - val sessionGroupDir = File(dataDir, getSessionGroupId(sessionId)) - if (!sessionGroupDir.exists()) { - sessionGroupDir.mkdirs() - } - return sessionGroupDir - } - - protected open fun getSessionGroupId(sessionId: String): String { - return sessionId.split("-").firstOrNull() ?: sessionId - } - - protected open fun getSessionInstanceId(sessionId: String): String { - return stripPrefix(stripPrefix(sessionId, getSessionGroupId(sessionId)), "-") - } - - - companion object { - fun stripPrefix(text: String, prefix: String): String { - val startsWith = text.toString().startsWith(prefix.toString()) - return if (startsWith) { - text.toString().substring(prefix.length) - } else { - text.toString() - } - } - } -} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionInterface.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionInterface.kt deleted file mode 100644 index 2fd9033d..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionInterface.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.simiacryptus.skyenet.body - -interface SessionInterface { - fun removeSocket(socket: WebSocketServer.MessageWebSocket) - fun addSocket(socket: WebSocketServer.MessageWebSocket) - fun getReplay(): List - fun onWebSocketText(socket: WebSocketServer.MessageWebSocket, message: String) -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetBasicChat.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetBasicChat.kt deleted file mode 100644 index b2d15138..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetBasicChat.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient - -open class SkyenetBasicChat( - applicationName: String, - oauthConfig: String? = null, - val model: OpenAIClient.Model = OpenAIClient.Models.GPT35Turbo, -) : SkyenetSessionServerBase( - applicationName = applicationName, - oauthConfig = oauthConfig, -) { - override val api: OpenAIClient - get() = OpenAIClient() - - override fun newSession(sessionId: String): SessionInterface { - val basicChatSession = BasicChatSession( - parent = this@SkyenetBasicChat, - model = model, - sessionId = sessionId - ) - val handler = MutableSessionHandler(null) - handler.setDelegate(basicChatSession) - return handler - } - -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSession.kt deleted file mode 100644 index f0ecbd5a..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSession.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.skyenet.Brain -import com.simiacryptus.skyenet.OutputInterceptor -import com.simiacryptus.skyenet.servers.SessionServerUtil.getHistory -import com.simiacryptus.skyenet.util.SessionServerUtil - -open class SkyenetCodingSession( - sessionId: String, - private val parent: SkyenetCodingSessionServer, -) : PersistentSessionBase(sessionId, parent.sessionDataStorage) { - - val history: MutableMap by lazy { - parent.sessionDataStorage.loadOperations(sessionId) - } - - override fun onCmd(id: String, code: String) { - history[id]?.onMessage(code) - parent.sessionDataStorage.updateOperationStatus(sessionId, id, history[id]!!) - super.onCmd(id, code) - } - - val hands = parent.hands() - val heart = parent.heart(hands) - open val brain by lazy { - object : Brain( - api = parent.api, - symbols = hands, - language = heart.getLanguage(), - describer = parent.typeDescriber, - model = parent.model - ) { - override fun getChatSystemMessages(apiDescription: String) = - if (parent.useHistory) { - super.getChatSystemMessages(apiDescription) + getHistory( - history.values, - 10, - parent.maxHistoryCharacters - ) - } else { - super.getChatSystemMessages(apiDescription) - } - - } - } - - override fun run( - userMessage: String, - ) { - OutputInterceptor.setupInterceptor() - logger.debug("${sessionId} - Processing message: $userMessage") - val operationID = (0..5).map { ('a'..'z').random() }.joinToString("") - val status = OperationStatus( - operationID = operationID, - instruction = userMessage, - thread = Thread.currentThread(), - ) - history[operationID] = status - parent.sessionDataStorage.updateOperationStatus(sessionId, operationID, status) - var retries = parent.maxRetries - var codedInstruction: String - var messageTrail = """$operationID,""" - val language = heart.getLanguage() - if (userMessage.startsWith("!!!")) { - codedInstruction = userMessage.substringAfter("!!!") - retries = 0 - //language=HTML - messageTrail += """

Code:

$codedInstruction
""" - } else { - //language=HTML - messageTrail += """

Command:

$userMessage
""" - //language=HTML - val triple = - implement(messageTrail, userMessage, language, status) - codedInstruction = triple.first - messageTrail += triple.second - } - - try { - while (retries >= 0 && !status.cancelFlag.get()) { - try { - if (status.cancelFlag.get()) { - status.status = OperationStatus.OperationState.Cancelled - break - } - if (!parent.autoRun) { - //language=HTML - send("""$messageTrail
""") - logger.debug("${sessionId} - Waiting for run") - status.runSemaphore.acquire() - logger.debug("${sessionId} - Run received") - } - if (status.cancelFlag.get()) { - status.status = OperationStatus.OperationState.Cancelled - break - } - send("""$messageTrail
${SkyenetSessionServerBase.spinner}
""") - status.status = OperationStatus.OperationState.Running - parent.sessionDataStorage.updateOperationStatus(sessionId, status.operationID, status) - messageTrail += execute(messageTrail, status, codedInstruction) - status.status = OperationStatus.OperationState.Complete - send(messageTrail) - break - } catch (e: Exception) { - logger.info("${sessionId} - Error", e) - //language=HTML - messageTrail += """

Error:

${parent.toString(e)}
""" - status.status = OperationStatus.OperationState.Error - status.resultOutput = OutputInterceptor.getGlobalOutput() - status.resultValue = parent.toString(e) - parent.sessionDataStorage.updateOperationStatus(sessionId, operationID, status) - if (retries <= 0 || status.cancelFlag.get()) { - //language=HTML - messageTrail += """

Out of Retries!

""" - logger.debug("${sessionId} - Out of retries") - this@SkyenetCodingSession.send(messageTrail) - break - } else { - retries-- - //language=HTML - val pair = fix( - messageTrail, - userMessage, - codedInstruction, - e, - language, - status - ) - codedInstruction = pair.first - messageTrail += pair.second - status.status = OperationStatus.OperationState.Implemented - parent.sessionDataStorage.updateOperationStatus(sessionId, status.operationID, status) - } - } - } - } catch (e: Exception) { - //language=HTML - messageTrail += """

Error:

${e.message}
""" - logger.warn("${sessionId} - Error: ${e.message}") - this@SkyenetCodingSession.send(messageTrail) - } finally { - parent.sessionDataStorage.updateOperationStatus(sessionId, operationID, status) - } - } - - override fun setMessage(key: String, value: String) : Int { - parent.sessionDataStorage.updateMessage(sessionId, key, value) - return super.setMessage(key, value) - } - - - open fun implement( - messageTrail: String, - describedInstruction: String, - language: String, - status: OperationStatus, - ): Triple { - //language=HTML - val buffer = StringBuffer() - buffer.append("""

Code:

""") - send("""$messageTrail$buffer${SkyenetSessionServerBase.spinner}
""") - val response = brain.implement(describedInstruction) - val codeBlocks = Brain.extractCodeBlocks(response) - var renderedResponse = SessionServerUtil.getRenderedResponse(codeBlocks) - var codedInstruction = SessionServerUtil.getCode(language, codeBlocks) - logger.debug("$sessionId - Response: $renderedResponse") - logger.debug("$sessionId - Code: $codedInstruction") - status.responseText = renderedResponse - status.responseCode = codedInstruction - buffer.append("""
${renderedResponse}
""") - - for (int in 0..3) { - try { - heart.validate(codedInstruction) - buffer.append("""""") - break - } catch (ex: Throwable) { - buffer.append("""
${codedInstruction}
${ex.message}
""") - send("""$messageTrail$buffer${SkyenetSessionServerBase.spinner}""") - val respondWithCode = - brain.fixCommand(codedInstruction, ex, status.resultOutput, describedInstruction) - renderedResponse = SessionServerUtil.getRenderedResponse(respondWithCode.second) - codedInstruction = SessionServerUtil.getCode(language, respondWithCode.second) - buffer.append("""
${renderedResponse}
""") - logger.debug("$sessionId - Response: $renderedResponse") - logger.debug("$sessionId - Code: $codedInstruction") - } - } - status.status = OperationStatus.OperationState.Implemented - parent.sessionDataStorage.updateOperationStatus(sessionId, status.operationID, status) - //language=HTML - return Triple(codedInstruction, buffer.toString(), renderedResponse) - } - - open fun fix( - messageTrail: String, - describedInstruction: String, - codedInstruction: String, - e: Exception, - language: String, - status: OperationStatus, - ): Pair { - //language=HTML - send("""$messageTrail

New Code:

${SkyenetSessionServerBase.spinner}
""") - val respondWithCode = - brain.fixCommand(codedInstruction, e, status.resultOutput, describedInstruction) - val renderedResponse = SessionServerUtil.getRenderedResponse(respondWithCode.second) - val newCode = SessionServerUtil.getCode(language, respondWithCode.second) - logger.debug("$sessionId - Response: $renderedResponse") - logger.debug("$sessionId - Code: $newCode") - status.responseText = renderedResponse - status.responseCode = newCode - //language=HTML - return Pair(newCode, """

New Code:

$renderedResponse
""") - } - - open fun execute( - messageTrail: String, - status: OperationStatus, - codedInstruction: String, - ): String { - //language=HTML - logger.info("$sessionId - Running $codedInstruction") - OutputInterceptor.clearGlobalOutput() - val result = heart.run(codedInstruction) - logger.info("$sessionId - Result: $result") - status.resultValue = result.toString() - val output = OutputInterceptor.getGlobalOutput() - status.resultOutput = output - //language=HTML - return """

Output:

$output

Returns:

${result}
""" - } - companion object { - val logger = org.slf4j.LoggerFactory.getLogger(SkyenetCodingSessionServer::class.java) - } -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSessionServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSessionServer.kt deleted file mode 100644 index e66dda1c..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetCodingSessionServer.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.Brain -import com.simiacryptus.skyenet.Heart -import com.simiacryptus.util.describe.AbbrevWhitelistYamlDescriber -import com.simiacryptus.util.describe.TypeDescriber -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.webapp.WebAppContext - -abstract class SkyenetCodingSessionServer( - applicationName: String, - val typeDescriber: TypeDescriber = AbbrevWhitelistYamlDescriber("com.simiacryptus", "com.github.simiacryptus"), - oauthConfig: String? = null, - val autoRun: Boolean = false, - resourceBase: String = "simpleSession", - val maxRetries: Int = 5, - var maxHistoryCharacters: Int = 4000, - temperature: Double = 0.1, - val model: OpenAIClient.Model = OpenAIClient.Models.GPT4, - var useHistory: Boolean = true, - private val shortExceptions: Boolean = false, - val apiKey: String -) : SkyenetSessionServerBase( - applicationName = applicationName, - oauthConfig = oauthConfig, - resourceBase = resourceBase, - temperature = temperature, -) { - - override val api: OpenAIClient = OpenAIClient(apiKey) - - abstract fun hands(): java.util.Map - abstract fun heart(hands: java.util.Map): Heart - - override fun configure(context: WebAppContext, prefix: String, baseUrl: String) { - super.configure(context, prefix, baseUrl) - context.addServlet(descriptorServlet, prefix + "yamlDescriptor") - } - - protected open val descriptorServlet = ServletHolder( - "yamlDescriptor", - object : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - resp.contentType = "text/plain" - resp.status = HttpServletResponse.SC_OK - val apiDescription = Brain.apiDescription(hands(), typeDescriber) - resp.writer.write(apiDescription) - } - }) - - override fun newSession(sessionId: String) = SkyenetCodingSession(sessionId, this) - - companion object { - val logger = org.slf4j.LoggerFactory.getLogger(SkyenetCodingSessionServer::class.java) - } - - open fun toString(e: Throwable) : String { - return if(shortExceptions) { - e.message ?: e.toString() - } else - { - val sw = java.io.StringWriter() - e.printStackTrace(java.io.PrintWriter(sw)) - sw.toString() - } - } - -} - diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetInterviewer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetInterviewer.kt deleted file mode 100644 index 12ca752d..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetInterviewer.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.util.describe.TypeDescriber -import com.simiacryptus.util.describe.YamlDescriber -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.webapp.WebAppContext - -@Suppress("unused") -open class SkyenetInterviewer( - applicationName: String, - val dataClass: Class, - val visiblePrompt: String, - val describer: TypeDescriber = YamlDescriber(), - oauthConfig: String? = null, - val continueSession: (String, T) -> SessionInterface, - val validate: (T) -> List, - private val apiKey: String -) : SkyenetSessionServerBase( - applicationName = applicationName, - oauthConfig = oauthConfig, -) { - - override val api: OpenAIClient = OpenAIClient(apiKey) - - override fun configure(context: WebAppContext, prefix: String, baseUrl: String) { - super.configure(context, prefix, baseUrl) - context.addServlet( - ServletHolder( - "yamlDescriptor", - object : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - resp.contentType = "text/plain" - resp.status = HttpServletResponse.SC_OK - resp.writer.write(describer.describe(dataClass)) - } - }), prefix + "yamlDescriptor" - ) - } - - - override fun newSession(sessionId: String): SessionInterface { - val handler = MutableSessionHandler(null) - val interviewSession: InterviewSession = object : InterviewSession( - parent = this@SkyenetInterviewer, - sessionId = sessionId, // This may need to be a new session id - dataClass = dataClass, - describer = this@SkyenetInterviewer.describer, - visiblePrompt = this@SkyenetInterviewer.visiblePrompt, - isFinished = this@SkyenetInterviewer.validate - ) { - override fun onFinished(data: T) { - handler.setDelegate(continueSession(sessionId, data)) - } - - override fun onUpdate(data: T) { - } - } - handler.setDelegate(interviewSession) - return handler - } - -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMacroChat.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMacroChat.kt deleted file mode 100644 index ed1f006e..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMacroChat.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.util.JsonUtil -import java.util.function.Consumer - -abstract class SkyenetMacroChat( - applicationName: String, - oauthConfig: String? = null, - temperature: Double = 0.1, -) : SkyenetSessionServerBase( - applicationName = applicationName, - oauthConfig = oauthConfig, - temperature = temperature, -) { - override val api: OpenAIClient - get() = OpenAIClient() - - interface SessionUI { - val spinner: String - val playButton: String - val cancelButton: String - val regenButton: String - fun hrefLink(handler: Consumer): String - fun textInput(handler: Consumer): String - } - - override fun newSession(sessionId: String): SessionInterface { - val handler = MutableSessionHandler(null) - handler.setDelegate(MacroChatSession(this,sessionId)) - return handler - } - - abstract fun processMessage( - sessionId: String, - userMessage: String, - session: PersistentSessionBase, - sessionUI: SessionUI, - sessionDiv: SessionDiv - ) - - companion object { - - @Suppress("unused") - fun iterate( - sessionUI: SessionUI, - sessionDiv: SessionDiv, - parameters: T, - feedbackFn: (msg: T, feedback: String) -> Unit, - fns: Map Unit> = mapOf(), - toString: (obj: T) -> String = { data: Any -> """
${JsonUtil.toJson(data)}
""" } - ) = sessionDiv.append( - """ - ${toString(parameters)} -
- ${sessionUI.textInput { feedbackFn(parameters, it) }} -
- ${fns.entries.joinToString("\n") { (label, fn) -> - sessionUI.hrefLink { fn(parameters) } + label + "
" - }} - """, false - ) - - } - -} - -abstract class SessionDiv { - abstract fun append(htmlToAppend: String, showSpinner: Boolean) : Unit - abstract fun sessionID(): String - abstract fun divID(): String -} - diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMarkupChat.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMarkupChat.kt deleted file mode 100644 index 2ffec480..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetMarkupChat.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.vladsch.flexmark.util.data.MutableDataSet -import java.awt.Desktop -import java.net.URI - -open class SkyenetMarkupChat( - applicationName: String, - oauthConfig: String? = null, - val visiblePrompt: String = """ - |Hello! I am here to assist you in a casual conversation! - |Feel free to ask me anything or just chat about your day. - """.trimMargin(), - val hiddenPrompt: String = """ - |I understand that the user might want to have a casual conversation. - |So, I'll respond in a friendly and engaging manner. - |I will also ask questions to keep the conversation going. - |If I mention any topics that can be cross-referenced on Wikipedia, I'll provide a link. - |Once we have finished our conversation, I'll say goodbye. - | - |${visiblePrompt} - """.trimMargin(), - val systemPrompt: String = """ - |You are a friendly and conversational AI that engages in casual chat with users. - |Your task is to respond to the user's messages in a friendly and engaging manner. - |Ask questions to keep the conversation going. - |Say goodbye when the conversation is over. - """.trimMargin() -) : SkyenetSessionServerBase( - applicationName = applicationName, - oauthConfig = oauthConfig, -) { - override val api: OpenAIClient - get() = OpenAIClient() - - override fun newSession(sessionId: String): SessionInterface { - val handler = MutableSessionHandler(null) - val basicChatSession = object : ChatSessionFlexmark( - parent = this@SkyenetMarkupChat, - sessionId = sessionId, - visiblePrompt = visiblePrompt, - hiddenPrompt = hiddenPrompt, - systemPrompt = systemPrompt, - ) { - override fun renderResponse(raw: String) = postRender(raw, super.renderResponse(raw)) - - override fun flexmarkOptions() = editFlexmarkOptions(super.flexmarkOptions()) - } - handler.setDelegate(basicChatSession) - return handler - } - - open fun postRender(raw: String, rendered: String) = rendered - open fun editFlexmarkOptions(options: MutableDataSet = MutableDataSet()) = options - - companion object { - - val api = OpenAIClient(OpenAIClient.keyTxt) - val log = org.slf4j.LoggerFactory.getLogger(SkyenetMarkupChat::class.java)!! - var sessionDataStorage: SessionDataStorage? = null - const val port = 8081 - private const val baseURL = "http://localhost:$port" - var skyenet = SkyenetMarkupChat( - applicationName = "Chat Demo" - ) - - @JvmStatic - fun main(args: Array) { - val httpServer = skyenet.start(port) - sessionDataStorage = skyenet.sessionDataStorage - Desktop.getDesktop().browse(URI(baseURL)) - httpServer.join() - } - } - -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetSessionServerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetSessionServerBase.kt deleted file mode 100644 index 51ea27b3..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SkyenetSessionServerBase.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.simiacryptus.skyenet.body - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.util.JsonUtil -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.apache.commons.io.FileUtils -import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.webapp.WebAppContext -import java.io.File -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -abstract class SkyenetSessionServerBase( - override val applicationName: String, - val oauthConfig: String? = null, - resourceBase: String = "simpleSession", - val temperature: Double = 0.1, -) : WebSocketServer(resourceBase) { - - abstract val api: OpenAIClient - - open val sessionDataStorage = SessionDataStorage(File(File(".skynet"), applicationName)) - - override fun configure(context: WebAppContext, prefix: String, baseURL: String) { - super.configure(context, prefix, baseURL) - - if (null != oauthConfig) (AuthenticatedWebsite("$baseURL/oauth2callback", this@SkyenetSessionServerBase.applicationName) { - FileUtils.openInputStream(File(oauthConfig)) - }).configure(context) - - context.addServlet(appInfo, prefix+"appInfo") - context.addServlet(fileIndex, prefix+"fileIndex/*") - context.addServlet(fileZip, prefix+"fileZip") - context.addServlet(sessionList, prefix+"sessions") - } - - protected open val fileZip = ServletHolder( - "fileZip", - object : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - val sessionID = req.getParameter("session") - val path = req.parameterMap.get("path")?.find { it.isNotBlank() } ?: "/" - val sessionDir = sessionDataStorage.getSessionDir(sessionID) - val file = File(sessionDir, path) - val zipFile = File.createTempFile("skynet", ".zip") - try { - zipFile.deleteOnExit() - zipFile.outputStream().use { outputStream -> - val zip = ZipOutputStream(outputStream) - write(file, file, zip) - zip.close() - } - resp.contentType = "application/zip" - resp.status = HttpServletResponse.SC_OK - resp.outputStream.write(zipFile.readBytes()) - } finally { - zipFile.delete() - } - } - - private fun write(basePath: File, file: File, zip: ZipOutputStream) { - if (file.isFile) { - val path = basePath.toURI().relativize(file.toURI()).path - zip.putNextEntry(ZipEntry(path)) - zip.write(file.readBytes()) - zip.closeEntry() - } else { - file.listFiles()?.forEach { - write(basePath, it, zip) - } - } - } - }) - - protected open val fileIndex = ServletHolder( - "fileIndex", - object : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - val path = req.pathInfo ?: "/" - val pathSegments = path.split("/").filter { it.isNotBlank() } - pathSegments.forEach { - if (it == "..") throw IllegalArgumentException("Invalid path") - } - val sessionID = pathSegments.first() - val sessionDir = sessionDataStorage.getSessionDir(sessionID) - val filePath = pathSegments.drop(1).joinToString("/") - val file = File(sessionDir, filePath) - if (file.isFile) { - resp.contentType = if (file.name.endsWith(".html")) { - "text/html" - } else if (file.name.endsWith(".json")) { - "application/json" - } else if (file.name.endsWith(".js")) { - "application/json" - } else if (file.name.endsWith(".png")) { - "image/png" - } else if (file.name.endsWith(".jpg")) { - "image/jpeg" - } else if (file.name.endsWith(".jpeg")) { - "image/jpeg" - } else if (file.name.endsWith(".gif")) { - "image/gif" - } else if (file.name.endsWith(".svg")) { - "image/svg+xml" - } else if (file.name.endsWith(".css")) { - "text/css" - } else { - "text/plain" - } - resp.status = HttpServletResponse.SC_OK - file.inputStream().use { inputStream -> - resp.outputStream.use { outputStream -> - inputStream.copyTo(outputStream) - } - } - } else { - resp.contentType = "text/html" - resp.status = HttpServletResponse.SC_OK - val files = file.listFiles()?.filter { it.isFile }?.sortedBy { it.name }?.joinToString("
") { - """${it.name}""" - } ?: "" - val folders = file.listFiles()?.filter { !it.isFile }?.sortedBy { it.name }?.joinToString("
") { - """${it.name}""" - } ?: "" - resp.writer.write( - """ - | - | - |Files - | - | - |

Archive

- |ZIP - |

Folders

- |$folders - |

Files

- |$files - | - | - """.trimMargin() - ) - } - } - }) - - inner class SessionServlet : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - resp.contentType = "text/html" - resp.status = HttpServletResponse.SC_OK - val sessions = sessionDataStorage.listSessions() - val links = sessions.joinToString("
") { - """ - |${sessionDataStorage.getSessionName(it)} - |
""".trimMargin() - } - resp.writer.write( - """ - | - | - |Sessions - | - | - |$links - | - | - """.trimMargin() - ) - } - } - protected open val sessionList = ServletHolder( - "sessionList", - SessionServlet() - ) - - - inner class AppInfoServlet : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - resp.contentType = "text/json" - resp.status = HttpServletResponse.SC_OK - resp.writer.write( - JsonUtil.objectMapper().writeValueAsString( - mapOf( - "applicationName" to applicationName - ) - ) - ) - } - } - protected open val appInfo = ServletHolder( - "appInfo", - AppInfoServlet() - ) - - companion object { - val logger = org.slf4j.LoggerFactory.getLogger(SkyenetSessionServerBase::class.java) - val spinner = - """
Loading...
""" - } - -} - diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/WebSocketServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/body/WebSocketServer.kt deleted file mode 100644 index cd3b76aa..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/WebSocketServer.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.simiacryptus.skyenet.body - -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.servlet.DefaultServlet -import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.util.resource.Resource -import org.eclipse.jetty.webapp.WebAppContext -import org.eclipse.jetty.websocket.api.Session -import org.eclipse.jetty.websocket.api.WebSocketAdapter -import org.eclipse.jetty.websocket.server.JettyWebSocketServlet -import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer -import java.util.* - -abstract class WebSocketServer(val resourceBase: String) { - - abstract val applicationName: String - - inner class NewSessionServlet : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - val sessionId = UUID.randomUUID().toString() - resp.contentType = "text/plain" - resp.status = HttpServletResponse.SC_OK - resp.writer.write(sessionId) - } - } - - val stateCache: MutableMap = mutableMapOf() - - inner class MessageWebSocket( - val sessionId: String, - private val sessionState: SessionInterface, - ) : WebSocketAdapter() { - - override fun onWebSocketConnect(session: Session) { - super.onWebSocketConnect(session) -// logger.debug("$sessionId - Socket connected: $session") - sessionState.addSocket(this) - sessionState.getReplay().forEach { - try { - remote.sendString(it) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - override fun onWebSocketText(message: String) { - super.onWebSocketText(message) - sessionState.onWebSocketText(this, message) - } - - override fun onWebSocketClose(statusCode: Int, reason: String?) { - super.onWebSocketClose(statusCode, reason) -// logger.debug("$sessionId - Socket closed: [$statusCode] $reason") - sessionState.removeSocket(this) - } - - override fun onWebSocketError(cause: Throwable) { - super.onWebSocketError(cause) -// logger.debug("$sessionId - WebSocket error: $cause") - } - - } - - inner class WebSocketHandler : JettyWebSocketServlet() { - override fun configure(factory: JettyWebSocketServletFactory) { - factory.setCreator { req, resp -> - val sessionId = req.parameterMap["sessionId"]?.firstOrNull() - return@setCreator if (null == sessionId) { -// logger.warn("No session ID provided") - null - } else { -// logger.debug("Creating socket for $sessionId") - MessageWebSocket(sessionId, stateCache.getOrPut(sessionId) { -// logger.debug("Creating session for $sessionId") - newSession(sessionId) - }) - } - } - } - } - - abstract fun newSession(sessionId: String): SessionInterface - - fun start(port: Int = 8080, baseUrl: String = "http://localhost:$port"): Server { - val server = Server(port) - configure(server, baseUrl) - server.start() - return server - } - - open fun configure(server: Server, baseUrl: String, path: String = "/") { - val webAppContext = WebAppContext() - webAppContext.baseResource = baseResource - webAppContext.contextPath = path - webAppContext.welcomeFiles = arrayOf("index.html") - JettyWebSocketServletContainerInitializer.configure(webAppContext, null) - configure(webAppContext, path, baseUrl) - server.handler = webAppContext - } - - open val baseResource: Resource? get() = Resource.newResource(javaClass.classLoader.getResource(resourceBase)) - - open fun configure(webAppContext: WebAppContext, prefix: String = "/", baseUrl: String) { - webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/default", defaultServlet), prefix + "") - webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/ws", webSocketHandler), prefix + "ws/*") - webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/newSession", newSessionServlet),prefix + "newSession") - } - - open val newSessionServlet get() = NewSessionServlet() - - open val webSocketHandler get() = WebSocketHandler() - - open val defaultServlet: DefaultServlet - get() { - val defaultServlet = DefaultServlet() - //defaultServlet.dirAllowed = false - return defaultServlet - } - - companion object { - val logger = org.slf4j.LoggerFactory.getLogger(WebSocketServer::class.java) - } -} - - diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt index fd2804f4..14ed91e0 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt @@ -1,10 +1,13 @@ package com.simiacryptus.skyenet.servers +import com.google.api.services.oauth2.model.Userinfo import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.skyenet.OutputInterceptor -import com.simiacryptus.skyenet.body.AuthenticatedWebsite -import com.simiacryptus.skyenet.body.WebSocketServer +import com.simiacryptus.skyenet.servlet.* +import com.simiacryptus.skyenet.sessions.ApplicationBase +import com.simiacryptus.skyenet.sessions.SessionDataStorage +import com.simiacryptus.skyenet.sessions.WebSocketServer import com.simiacryptus.skyenet.util.AwsUtil.decryptResource import jakarta.servlet.DispatcherType import jakarta.servlet.Servlet @@ -18,12 +21,15 @@ import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.util.resource.Resource import org.eclipse.jetty.webapp.WebAppContext import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer +import org.intellij.lang.annotations.Language import java.awt.Desktop import java.net.URI +import java.nio.file.NoSuchFileException import java.util.* abstract class AppServerBase( private val localName: String = "localhost", + private val publicName: String = "localhost", private val port: Int = 8081, ) { var domainName: String = "" @@ -32,11 +38,17 @@ abstract class AppServerBase( data class ChildWebApp( val path: String, val server: WebSocketServer, - val isAuthenticated: Boolean = false + val isAuthenticated: Boolean = false, + val isPublicOnly: Boolean = false ) private fun domainName(isServer: Boolean) = - if (isServer) "https://apps.simiacrypt.us" else "http://$localName:$port" + if (isServer) "https://$publicName" else "http://$localName:$port" + + val welcomeResources = Resource.newResource(javaClass.classLoader.getResource("welcome")) + val userInfoServlet = UserInfoServlet() + val userSettingsServlet = UserSettingsServlet() + val usageServlet = UsageServlet() protected fun _main(args: Array) { try { @@ -53,39 +65,19 @@ abstract class AppServerBase( val server = start( port, - *(arrayOf(authentication.configure( - newWebAppContext( - "/", - Resource.newResource(javaClass.classLoader.getResource("welcome")), - object : HttpServlet() { - override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) { - resp?.contentType = "text/html" - resp?.writer?.write(""" - - - - - SimiaCryptus Skyenet Apps - - - - - -
- ${ - childWebApps.joinToString("
") { - """${it.server.applicationName}""" - } - } -
- - - - """.trimIndent()) - } - } - ), false - ), newWebAppContext("/proxy", ProxyHttpServlet())) + childWebApps.map { + *(arrayOf( + newWebAppContext("/userInfo", userInfoServlet), + newWebAppContext("/userSettings", userSettingsServlet), + newWebAppContext("/usage", usageServlet), + newWebAppContext("/proxy", ProxyHttpServlet()), + authentication.configure( + newWebAppContext( + "/", + welcomeResources, + WelcomeServlet() + ), false + ), + ) + childWebApps.map { if (it.isAuthenticated) authentication.configure( newWebAppContext( it.path, @@ -110,6 +102,91 @@ abstract class AppServerBase( } } + inner class WelcomeServlet() : HttpServlet() { + override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) { + val user = AuthenticatedWebsite.getUser(req!!) + val requestURI = req.requestURI ?: "/" + resp?.contentType = when (requestURI) { + "/" -> "text/html" + else -> ApplicationBase.getMimeType(requestURI) + } + when { + requestURI == "/" -> resp?.writer?.write(homepage(user).trimIndent()) + requestURI == "/index.html" -> resp?.writer?.write(homepage(user).trimIndent()) + requestURI.startsWith("/userInfo") -> userInfoServlet.doGet(req, resp!!) + requestURI.startsWith("/userSettings") -> userSettingsServlet.doGet(req, resp!!) + requestURI.startsWith("/usage") -> usageServlet.doGet(req, resp!!) + else -> try { + val inputStream = welcomeResources.addPath(requestURI)?.inputStream + inputStream?.copyTo(resp?.outputStream!!) + } catch (e: NoSuchFileException) { + resp?.sendError(404) + } + } + } + + override fun doPost(req: HttpServletRequest?, resp: HttpServletResponse?) { + val requestURI = req?.requestURI ?: "/" + when { + requestURI.startsWith("/userSettings") -> userSettingsServlet.doPost(req!!, resp!!) + else -> resp?.sendError(404) + } + } + } + + private fun homepage(user: Userinfo?): String { + @Language("HTML") + val html = """ + + + + SimiaCryptus Skyenet Apps + + + + + + +
+
+ +
+ Login +
+ + + + + + + + ${ + childWebApps.filter { + (!it.isAuthenticated || user != null) && (!it.isPublicOnly || user == null) + }.joinToString("\n") { app -> + """ + + + + + + """.trimIndent() + } + } +
AppNew SessionList Sessions
+ ${app.server.applicationName} + + New + + List +
+ + + + """ + return html + } + private fun start( port: Int, vararg webAppContexts: WebAppContext @@ -126,8 +203,9 @@ abstract class AppServerBase( } private fun newWebAppContext(path: String, server: WebSocketServer): WebAppContext { - val webAppContext = newWebAppContext(path, server.baseResource ?: throw IllegalStateException("No base resource")) - server.configure(webAppContext, baseUrl = "$domainName/$path") + val webAppContext = + newWebAppContext(path, server.baseResource ?: throw IllegalStateException("No base resource")) + server.configure(webAppContext, path = path, baseUrl = "$domainName/$path") return webAppContext } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/CodingActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/CodingActorTestApp.kt index 3296db11..8f5eccdf 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/CodingActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/CodingActorTestApp.kt @@ -1,38 +1,39 @@ package com.simiacryptus.skyenet.servers import com.simiacryptus.skyenet.actors.CodingActor -import com.simiacryptus.skyenet.body.ChatSessionFlexmark.Companion.renderMarkdown -import com.simiacryptus.skyenet.body.PersistentSessionBase -import com.simiacryptus.skyenet.body.SessionDiv -import com.simiacryptus.skyenet.body.SkyenetMacroChat +import com.simiacryptus.skyenet.sessions.* +import com.simiacryptus.skyenet.util.HtmlTools +import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.util.* open class CodingActorTestApp( private val actor: CodingActor, - applicationName: String = "CodingActorTest_"+actor.interpreter.javaClass.simpleName, + applicationName: String = "CodingActorTest_" + actor.interpreter.javaClass.simpleName, temperature: Double = 0.3, oauthConfig: String? = null, -) : SkyenetMacroChat( +) : ChatApplicationBase( applicationName = applicationName, oauthConfig = oauthConfig, temperature = temperature, ) { + override fun processMessage( sessionId: String, userMessage: String, session: PersistentSessionBase, - sessionUI: SessionUI, - sessionDiv: SessionDiv + sessionDiv: SessionDiv, + socket: MessageWebSocket ) { sessionDiv.append("""
${renderMarkdown(userMessage)}
""", true) - val response = actor.answer(userMessage) + val response = actor.answer(userMessage, api = socket.api) sessionDiv.append("""
${ renderMarkdown(""" |```${actor.interpreter.getLanguage().lowercase(Locale.getDefault())} |${response.getCode()} |``` - |${sessionUI.hrefLink { + |${ + sessionDiv.htmlTools.hrefLink { sessionDiv.append("""
Running...
""", true) val result = response.run() sessionDiv.append( @@ -47,7 +48,9 @@ open class CodingActorTestApp( }
""", false) } + companion object { + val SessionDiv.htmlTools get() = HtmlTools(divID()) val log = LoggerFactory.getLogger(CodingActorTestApp::class.java) } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/EmbeddingVisualizer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/EmbeddingVisualizer.kt index 237d19c9..18ce2c77 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/EmbeddingVisualizer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/EmbeddingVisualizer.kt @@ -1,7 +1,7 @@ package com.simiacryptus.skyenet.servers import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.body.* +import com.simiacryptus.skyenet.sessions.* import com.simiacryptus.util.JsonUtil class EmbeddingVisualizer( diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ParsedActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ParsedActorTestApp.kt index 10e791be..34252828 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ParsedActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ParsedActorTestApp.kt @@ -1,19 +1,20 @@ package com.simiacryptus.skyenet.servers import com.simiacryptus.skyenet.actors.ParsedActor -import com.simiacryptus.skyenet.body.ChatSessionFlexmark.Companion.renderMarkdown -import com.simiacryptus.skyenet.body.PersistentSessionBase -import com.simiacryptus.skyenet.body.SessionDiv -import com.simiacryptus.skyenet.body.SkyenetMacroChat +import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown +import com.simiacryptus.skyenet.sessions.PersistentSessionBase +import com.simiacryptus.skyenet.sessions.SessionDiv +import com.simiacryptus.skyenet.sessions.ChatApplicationBase +import com.simiacryptus.skyenet.sessions.MessageWebSocket import com.simiacryptus.util.JsonUtil import org.slf4j.LoggerFactory open class ParsedActorTestApp( private val actor: ParsedActor, - applicationName: String = "ParsedActorTest_"+actor.parserClass.simpleName, + applicationName: String = "ParsedActorTest_" + actor.parserClass.simpleName, temperature: Double = 0.3, oauthConfig: String? = null, -) : SkyenetMacroChat( +) : ChatApplicationBase( applicationName = applicationName, oauthConfig = oauthConfig, temperature = temperature, @@ -22,11 +23,11 @@ open class ParsedActorTestApp( sessionId: String, userMessage: String, session: PersistentSessionBase, - sessionUI: SessionUI, - sessionDiv: SessionDiv + sessionDiv: SessionDiv, + socket: MessageWebSocket ) { sessionDiv.append("""
${renderMarkdown(userMessage)}
""", true) - val response = actor.answer(userMessage) + val response = actor.answer(userMessage, api = socket.api) sessionDiv.append("""
${ renderMarkdown(""" |${response.getText()} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt index dda32a51..09898679 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt @@ -1,32 +1,25 @@ package com.simiacryptus.skyenet.servers -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.body.* +import com.simiacryptus.skyenet.sessions.* import org.slf4j.LoggerFactory open class ReadOnlyApp( applicationName: String, temperature: Double = 0.3, oauthConfig: String? = null, -) : SkyenetSessionServerBase( +) : ApplicationBase( applicationName = applicationName, oauthConfig = oauthConfig, temperature = temperature, + resourceBase = "readOnly", ) { companion object { val log = LoggerFactory.getLogger(ReadOnlyApp::class.java) } - override val api: OpenAIClient - get() = OpenAIClient() - override fun newSession(sessionId: String): SessionInterface { - return BasicChatSession( - parent = this@ReadOnlyApp, - model = OpenAIClient.Models.GPT35Turbo, - sessionId = sessionId - ) + throw UnsupportedOperationException() } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SessionServerUtil.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SessionServerUtil.kt deleted file mode 100644 index ede37ed6..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SessionServerUtil.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.simiacryptus.skyenet.servers - -import com.simiacryptus.openai.OpenAIClient.ChatMessage -import com.simiacryptus.skyenet.body.OperationStatus - -object SessionServerUtil { - - val logger = org.slf4j.LoggerFactory.getLogger(SessionServerUtil::class.java) - - 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 getHistory(statuses: Collection, maxEntries: Int, maxChars: Int): List { - val messages: java.util.ArrayList = ArrayList() - val operationStatusList = statuses - .filter { it.status != OperationStatus.OperationState.Pending } - .filter { it.status != OperationStatus.OperationState.Running } - .filter { it.status != OperationStatus.OperationState.Implemented } - .filter { !it.instruction.startsWith("!!!") } - .sortedBy { it.created } - .takeLast(maxEntries) - for (operationStatus in operationStatusList) { - messages.addAll(operationStatus.chatMessages) - } - if (messages.isEmpty()) return messages - var totalCharacters = 0 - val truncatedMessages = mutableListOf() - for (message in messages.reversed()) { - totalCharacters += message.content?.length ?: 0 - if (totalCharacters > maxChars) break - truncatedMessages.add(message) - } - return truncatedMessages.reversed() - } - -} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SimpleActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SimpleActorTestApp.kt index 8ac6d9a6..de153d8b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SimpleActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/SimpleActorTestApp.kt @@ -1,33 +1,38 @@ package com.simiacryptus.skyenet.servers import com.simiacryptus.skyenet.actors.SimpleActor -import com.simiacryptus.skyenet.body.ChatSessionFlexmark -import com.simiacryptus.skyenet.body.PersistentSessionBase -import com.simiacryptus.skyenet.body.SessionDiv -import com.simiacryptus.skyenet.body.SkyenetMacroChat +import com.simiacryptus.skyenet.sessions.* +import com.simiacryptus.skyenet.util.MarkdownUtil import org.slf4j.LoggerFactory open class SimpleActorTestApp( private val actor: SimpleActor, - applicationName: String = "SimpleActorTest_"+actor.javaClass.simpleName, + applicationName: String = "SimpleActorTest_" + actor.javaClass.simpleName, temperature: Double = 0.3, oauthConfig: String? = null, -) : SkyenetMacroChat( +) : ChatApplicationBase( applicationName = applicationName, oauthConfig = oauthConfig, temperature = temperature, ) { + data class Settings( + val actor: SimpleActor? = null, + ) + override val settingsClass: Class<*> get() = Settings::class.java + @Suppress("UNCHECKED_CAST") override fun initSettings(sessionId: String): T? = Settings(actor=actor) as T + override fun processMessage( sessionId: String, userMessage: String, session: PersistentSessionBase, - sessionUI: SessionUI, - sessionDiv: SessionDiv + sessionDiv: SessionDiv, + socket: MessageWebSocket ) { - sessionDiv.append("""
${ChatSessionFlexmark.renderMarkdown(userMessage)}
""", true) - val moderatorResponse = actor.answer(userMessage) - sessionDiv.append("""
${ChatSessionFlexmark.renderMarkdown(moderatorResponse)}
""", false) + val actor = getSettings(sessionId)?.actor ?: actor + sessionDiv.append("""
${MarkdownUtil.renderMarkdown(userMessage)}
""", true) + val moderatorResponse = actor.answer(userMessage, api = socket.api) + sessionDiv.append("""
${MarkdownUtil.renderMarkdown(moderatorResponse)}
""", false) } companion object { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/AuthenticatedWebsite.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt similarity index 63% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/body/AuthenticatedWebsite.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt index 66e5b2b9..4c1b8d88 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/AuthenticatedWebsite.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt @@ -1,141 +1,150 @@ -package com.simiacryptus.skyenet.body - -import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow -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 jakarta.servlet.* -import jakarta.servlet.http.Cookie -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.eclipse.jetty.servlet.FilterHolder -import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.webapp.WebAppContext -import java.io.InputStream -import java.io.InputStreamReader -import java.util.* - -open class AuthenticatedWebsite( - val redirectUri: String, - val applicationName: String, - private val key: ()->InputStream? -) { - - open fun newUserSession(userInfo: Userinfo, sessionId: String) { - logger.info("User $userInfo logged in with session $sessionId") - } - - 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, - listOf( - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile" - ) - ).build() - - - inner class GoogleLoginServlet : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - val authorizationUrl = flow.newAuthorizationUrl().setRedirectUri(redirectUri).build() - resp.sendRedirect(authorizationUrl) - } - } - - /** - * Configures the given `WebAppContext` to handle authentication via Google OAuth2. - * Adds the `GoogleLoginServlet` to handle the login process at `/googleLogin`. - * Adds the `OAuth2CallbackServlet` to handle the callback from Google at `/oauth2callback`. - * Adds a `SessionIdFilter` to the context to ensure that session IDs are properly handled. - * - * @param context the `WebAppContext` to configure - */ - 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)) - return context - } - - inner class SessionIdFilter : Filter { - - override fun init(filterConfig: FilterConfig?) {} - - /** - * Overrides the doFilter method to intercept incoming requests and responses, - * check if the request is for the "/googleLogin" or "/oauth2callback" URLs, - * and redirects to the "/googleLogin" page if the user session is not active. - * - * @param request The ServletRequest object that represents the client request - * @param response The ServletResponse object that represents the client response - * @param chain The FilterChain object that represents the filter chain - * @throws IOException if an I/O error occurs - * @throws ServletException if a servlet-specific error occurs - */ - override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { - // Check if the request is an HTTP request and the response is an HTTP response - if (request is HttpServletRequest && response is HttpServletResponse) { - // Get the path of the request URI - // Check if the path does not match /googleLogin or /oauth2callback - if (isSecure(request)) { - // Get the sessionId cookie from the request - val sessionIdCookie = request.cookies?.firstOrNull { it.name == "sessionId" } - // Check if the sessionId is null or is not contained in the users map - if (sessionIdCookie == null || !users.containsKey(sessionIdCookie.value)) { - // Redirect to the /googleLogin endpoint - response.sendRedirect("/googleLogin") - return - } - } - } - // Call the doFilter method of the next filter in the chain - chain.doFilter(request, response) - } - - override fun destroy() {} - } - - open fun isSecure(request: HttpServletRequest) = - setOf("/googleLogin", "/oauth2callback").none { request.requestURI.startsWith(it) } - - val users = HashMap() - - inner class OAuth2CallbackServlet : 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() - users[sessionID] = userInfo - newUserSession(userInfo, sessionID) - val sessionCookie = Cookie("sessionId", sessionID) - sessionCookie.path = "/" - sessionCookie.isHttpOnly = false - resp.addCookie(sessionCookie) - resp.sendRedirect("/") - } else { - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Authorization code not found") - } - } - } - - companion object { - private val logger = org.slf4j.LoggerFactory.getLogger(AuthenticatedWebsite::class.java) - } - -} \ No newline at end of file +package com.simiacryptus.skyenet.servlet + +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl +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 jakarta.servlet.* +import jakarta.servlet.http.Cookie +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.eclipse.jetty.servlet.FilterHolder +import org.eclipse.jetty.servlet.ServletHolder +import org.eclipse.jetty.webapp.WebAppContext +import java.io.InputStream +import java.io.InputStreamReader +import java.io.UnsupportedEncodingException +import java.net.URI +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.util.* + + +open class AuthenticatedWebsite( + val redirectUri: String, + val applicationName: String, + key: () -> InputStream? +) { + + open fun newUserSession(userInfo: Userinfo, sessionId: String) { + log.info("User $userInfo logged in with session $sessionId") + } + + 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, + listOf( + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile" + ) + ).build() + + + inner class GoogleLoginServlet : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + val redirect = req.getParameter("redirect") ?: "" + val state = URLEncoder.encode(redirect, StandardCharsets.UTF_8.toString()) + val authorizationUrl = // don't want to specify redirectUri to give control of it to user of this class + GoogleAuthorizationCodeRequestUrl( + /* authorizationServerEncodedUrl = */ flow.authorizationServerEncodedUrl, + /* clientId = */ flow.clientId, + /* redirectUri = */ redirectUri, + /* scopes = */ flow.scopes + ) + .setAccessType(flow.accessType) + .setApprovalPrompt(flow.approvalPrompt) + .setState(state) + .build() + resp.sendRedirect(authorizationUrl) + } + } + + 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)) + return context + } + + 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.cookies?.firstOrNull { it.name == "sessionId" } + if (sessionIdCookie == null || !users.containsKey(sessionIdCookie.value)) { + response.sendRedirect("/googleLogin") + return + } + } + } + chain.doFilter(request, response) + } + + override fun destroy() {} + } + + open fun isSecure(request: HttpServletRequest) = + setOf("/googleLogin", "/oauth2callback").none { request.requestURI.startsWith(it) } + + + inner class OAuth2CallbackServlet : 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() + users[sessionID] = userInfo + newUserSession(userInfo, sessionID) + val sessionCookie = Cookie("sessionId", sessionID) + sessionCookie.path = "/" + sessionCookie.isHttpOnly = false + resp.addCookie(sessionCookie) + val redirect = req.getParameter("state")?.urlDecode() + resp.sendRedirect(redirect ?: "/") + } else { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Authorization code not found") + } + } + } + + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(AuthenticatedWebsite::class.java) + + + val users = java.util.HashMap() + fun getUser(req: HttpServletRequest): Userinfo? { + val sessionId = req.cookies?.find { it.name == "sessionId" }?.value + return if (null == sessionId) null else users[sessionId] + } + + } + +} + +fun String.urlDecode(): String? = try { + URLDecoder.decode(this, StandardCharsets.UTF_8.toString()) +} catch (e: UnsupportedEncodingException) { + this +} + +fun String.toURI(): URI = URI(this) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt new file mode 100644 index 00000000..7774e28c --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt @@ -0,0 +1,58 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.skyenet.sessions.ApplicationBase +import com.simiacryptus.skyenet.sessions.SessionDataStorage +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import java.io.File + +class FileServlet(val sessionDataStorage: SessionDataStorage) : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + val path = req.pathInfo ?: "/" + val pathSegments = path.split("/").filter { it.isNotBlank() } + pathSegments.forEach { + if (it == "..") throw IllegalArgumentException("Invalid path") + } + val sessionID = pathSegments.first() + val sessionDir = sessionDataStorage.getSessionDir(sessionID) + val filePath = pathSegments.drop(1).joinToString("/") + val file = File(sessionDir, filePath) + if (file.isFile) { + val filename = file.name + resp.contentType = ApplicationBase.getMimeType(filename) + resp.status = HttpServletResponse.SC_OK + file.inputStream().use { inputStream -> + resp.outputStream.use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } else { + resp.contentType = "text/html" + resp.status = HttpServletResponse.SC_OK + val files = file.listFiles()?.filter { it.isFile }?.sortedBy { it.name }?.joinToString("
") { + """${it.name}""" + } ?: "" + val folders = file.listFiles()?.filter { !it.isFile }?.sortedBy { it.name }?.joinToString("
") { + """${it.name}""" + } ?: "" + resp.writer.write( + """ + | + | + |Files + | + | + |

Archive

+ |ZIP + |

Folders

+ |$folders + |

Files

+ |$files + | + | + """.trimMargin() + ) + } + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt new file mode 100644 index 00000000..2d668eda --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt @@ -0,0 +1,15 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.skyenet.sessions.SessionDataStorage +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +class NewSessionServlet : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + val sessionId = SessionDataStorage.newID() + resp.contentType = "text/plain" + resp.status = HttpServletResponse.SC_OK + resp.writer.write(sessionId) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ProxyHttpServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ProxyHttpServlet.kt similarity index 98% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ProxyHttpServlet.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ProxyHttpServlet.kt index 7d5519be..61137d60 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ProxyHttpServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ProxyHttpServlet.kt @@ -1,4 +1,4 @@ -package com.simiacryptus.skyenet.servers +package com.simiacryptus.skyenet.servlet import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt new file mode 100644 index 00000000..07f911d7 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt @@ -0,0 +1,33 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.skyenet.sessions.SessionDataStorage +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +class SessionServlet(private val sessionDataStorage: SessionDataStorage, val prefix: String) : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/html" + resp.status = HttpServletResponse.SC_OK + val sessions = sessionDataStorage.listSessions() + // onclick="window.location.href='#$session';window.location.reload();" + val links = sessions.joinToString("
") { session -> + val sessionName = sessionDataStorage.getSessionName(session) + """ + |$sessionName + |
""".trimMargin() + } + resp.writer.write( + """ + | + | + |Sessions + | + | + |$links + | + | + """.trimMargin() + ) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt new file mode 100644 index 00000000..6713bb8a --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt @@ -0,0 +1,57 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.skyenet.sessions.ApplicationBase +import com.simiacryptus.util.JsonUtil +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +class SessionSettingsServlet( + private val server: ApplicationBase, +) : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/html" + resp.status = HttpServletResponse.SC_OK + val sessionId = req.getParameter("sessionId") + if (null != sessionId) { + val settings = server.getSettings(sessionId) + val json = if(settings != null) JsonUtil.toJson(settings) else "" + //language=HTML + resp.writer.write( + """ + | + | + | Settings + | + | + | + |
+ | + | + | + | + |
+ | + | + """.trimMargin() + ) + } else { + resp.status = HttpServletResponse.SC_BAD_REQUEST + resp.writer.write("Session ID is required") + } + } + + override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/html" + resp.status = HttpServletResponse.SC_OK + val sessionId = req.getParameter("sessionId") + if (null == sessionId) { + resp.status = HttpServletResponse.SC_BAD_REQUEST + resp.writer.write("Session ID is required") + } else { + val settings = JsonUtil.fromJson(req.getParameter("settings"), server.settingsClass) + server.sessionDataStorage.updateSettings(sessionId, settings) + resp.sendRedirect("${req.contextPath}/#$sessionId") + } + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt new file mode 100644 index 00000000..0ffdaaf2 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt @@ -0,0 +1,107 @@ +package com.simiacryptus.skyenet.servlet + +import com.google.api.services.oauth2.model.Userinfo +import com.simiacryptus.openai.OpenAIClient +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import java.util.concurrent.atomic.AtomicInteger + +class UsageServlet : HttpServlet() { + public override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/html" + resp.status = HttpServletResponse.SC_OK + + val sessionId = req.getParameter("sessionId") + if (null != sessionId) { + serve(resp, getSessionUsageSummary(sessionId)) + } else { + val userinfo = AuthenticatedWebsite.getUser(req) + if (null == userinfo) { + resp.status = HttpServletResponse.SC_BAD_REQUEST + } else { + val usage = getUserUsageSummary(userinfo) + serve(resp, usage) + } + } + } + + private fun serve( + resp: HttpServletResponse, + usage: Map + ) { + resp.writer.write( + """ + | + | + | Usage + | + | + | + | + | + | + | + | + | ${ + usage.entries.joinToString("\n") { (model, count) -> + """ + | + | + | + | + """.trimMargin() + } + } + |
ModelUsage
$model$count
+ | + | + """.trimMargin() + ) + } + + data class UsageCounters( + val tokensPerModel: HashMap = HashMap(), + ) + + companion object { + val log = org.slf4j.LoggerFactory.getLogger(UsageServlet::class.java) + + private val usagePerSession = HashMap() + val sessionsByUser = HashMap>() + + fun incrementUsage(sessionId: String, userinfo: Userinfo?, model: OpenAIClient.Model, tokens: Int) { + val usage = usagePerSession.getOrPut(sessionId) { + UsageCounters() + } + val tokensPerModel = usage.tokensPerModel.getOrPut(model) { + AtomicInteger() + } + tokensPerModel.addAndGet(tokens) + if (userinfo != null) { + val sessions = sessionsByUser.getOrPut(userinfo.id) { + ArrayList() + } + sessions.add(sessionId) + } + } + + fun getUserUsageSummary(userinfo: Userinfo): Map { + val sessions = sessionsByUser[userinfo.id] + return sessions?.flatMap { sessionId -> + val usage = usagePerSession[sessionId] + usage?.tokensPerModel?.entries?.map { (model, counter) -> + model to counter.get() + } ?: emptyList() + }?.groupBy { it.first }?.mapValues { it.value.map { it.second }.sum() } ?: emptyMap() + } + + fun getSessionUsageSummary(sessionId: String): Map { + val usage = usagePerSession[sessionId] + return usage?.tokensPerModel?.entries?.map { (model, counter) -> + model to counter.get() + }?.groupBy { it.first }?.mapValues { it.value.map { it.second }.sum() } ?: emptyMap() + } + + } +} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt new file mode 100644 index 00000000..5cc5dbe6 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt @@ -0,0 +1,19 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.util.JsonUtil +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +class UserInfoServlet : HttpServlet() { + public override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/json" + resp.status = HttpServletResponse.SC_OK + val userinfo = AuthenticatedWebsite.getUser(req) + if (null == userinfo) { + resp.writer.write("{}") + } else { + resp.writer.write(JsonUtil.objectMapper().writeValueAsString(userinfo)) + } + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt new file mode 100644 index 00000000..08df2d06 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt @@ -0,0 +1,75 @@ +package com.simiacryptus.skyenet.servlet + +import com.google.api.services.oauth2.model.Userinfo +import com.simiacryptus.util.JsonUtil +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + +class UserSettingsServlet : HttpServlet() { + public override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/html" + resp.status = HttpServletResponse.SC_OK + val authId = req.cookies?.firstOrNull { it.name == "sessionId" }?.value + if (null == authId) { + resp.status = HttpServletResponse.SC_BAD_REQUEST + } else { + val userinfo = AuthenticatedWebsite.users[authId] + if(null == userinfo) { + resp.status = HttpServletResponse.SC_BAD_REQUEST + } else { + val settings = getUserSettings(userinfo) + val json = if (settings != null) JsonUtil.toJson(settings) else "" + //language=HTML + resp.writer.write( + """ + | + | + | Settings + | + | + | + |
+ | + | + | + | + |
+ | + | + """.trimMargin() + ) + } + } + } + + public override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { + val userinfo = AuthenticatedWebsite.getUser(req) + if (null == userinfo) { + resp.status = HttpServletResponse.SC_BAD_REQUEST + } else { + val settings = JsonUtil.fromJson(req.getParameter("settings"), UserSettings::class.java) + updateUserSettings(userinfo, settings) + resp.sendRedirect("/") + } + } + + companion object { + val log = org.slf4j.LoggerFactory.getLogger(UserSettingsServlet::class.java) + private val userSettings = HashMap() + + fun getUserSettings(userinfo: Userinfo): UserSettings { + return userSettings.getOrPut(userinfo.id) { + UserSettings() + } + } + + fun updateUserSettings(userinfo: Userinfo, settings: UserSettings) { + userSettings[userinfo.id] = settings + } + + } +} +data class UserSettings( + val apiKey: String = "", +) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt new file mode 100644 index 00000000..86ce03df --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt @@ -0,0 +1,45 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.skyenet.sessions.SessionDataStorage +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class ZipServlet(val sessionDataStorage: SessionDataStorage) : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + val sessionID = req.getParameter("session") + val path = req.parameterMap.get("path")?.find { it.isNotBlank() } ?: "/" + val sessionDir = sessionDataStorage.getSessionDir(sessionID) + val file = File(sessionDir, path) + val zipFile = File.createTempFile("skynet", ".zip") + try { + zipFile.deleteOnExit() + zipFile.outputStream().use { outputStream -> + val zip = ZipOutputStream(outputStream) + write(file, file, zip) + zip.close() + } + resp.contentType = "application/zip" + resp.status = HttpServletResponse.SC_OK + resp.outputStream.write(zipFile.readBytes()) + } finally { + zipFile.delete() + } + } + + private fun write(basePath: File, file: File, zip: ZipOutputStream) { + if (file.isFile) { + val path = basePath.toURI().relativize(file.toURI()).path + zip.putNextEntry(ZipEntry(path)) + zip.write(file.readBytes()) + zip.closeEntry() + } else { + file.listFiles()?.forEach { + write(basePath, it, zip) + } + } + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ApplicationBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ApplicationBase.kt new file mode 100644 index 00000000..f9ec4f8c --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ApplicationBase.kt @@ -0,0 +1,105 @@ +package com.simiacryptus.skyenet.sessions + +import com.simiacryptus.skyenet.servlet.* +import com.simiacryptus.util.JsonUtil +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.apache.commons.io.FileUtils +import org.eclipse.jetty.servlet.ServletHolder +import org.eclipse.jetty.webapp.WebAppContext +import org.slf4j.LoggerFactory +import java.io.File + +abstract class ApplicationBase( + final override val applicationName: String, + val oauthConfig: String? = null, + resourceBase: String = "simpleSession", + val temperature: Double = 0.1, +) : WebSocketServer(resourceBase) { + + open val settingsClass: Class<*> get() = Map::class.java + + open fun initSettings(sessionId: String): T? = null + + fun getSettings(sessionId: String): T? { + @Suppress("UNCHECKED_CAST") + var settings: T? = sessionDataStorage.getSettings(sessionId, settingsClass as Class) + if (null == settings) { + settings = initSettings(sessionId) + if (null != settings) { + sessionDataStorage.updateSettings(sessionId, settings) + } + } + return settings + } + + fun updateSettings(sessionId: String, settings: T) { + sessionDataStorage.updateSettings(sessionId, settings) + } + + final override val sessionDataStorage = SessionDataStorage(File(File(".skynet"), applicationName)) + + + override fun configure(webAppContext: WebAppContext, path: String, baseUrl: String) { + super.configure(webAppContext, path, baseUrl) + + if (null != oauthConfig) (AuthenticatedWebsite( + "$baseUrl/oauth2callback", + this@ApplicationBase.applicationName + ) { + FileUtils.openInputStream(File(oauthConfig)) + }).configure(webAppContext) + + val fileZip = ServletHolder("fileZip", ZipServlet(sessionDataStorage)) + val fileIndex = ServletHolder("fileIndex", FileServlet(sessionDataStorage)) + val sessionsServlet = ServletHolder("sessionList", SessionServlet(this.sessionDataStorage, path)) + val sessionSettingsServlet = ServletHolder("settings", SessionSettingsServlet(this)) + + webAppContext.addServlet(appInfo, "/appInfo") + webAppContext.addServlet(userInfo, "/userInfo") + webAppContext.addServlet(fileIndex, "/fileIndex/*") + webAppContext.addServlet(fileZip, "/fileZip") + webAppContext.addServlet(sessionsServlet, "/sessions") + webAppContext.addServlet(sessionSettingsServlet, "/settings") + } + + inner class AppInfoServlet : HttpServlet() { + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = "text/json" + resp.status = HttpServletResponse.SC_OK + resp.writer.write( + JsonUtil.objectMapper().writeValueAsString( + mapOf( + "applicationName" to applicationName + ) + ) + ) + } + } + + protected open val appInfo = ServletHolder("appInfo", AppInfoServlet()) + protected open val userInfo = ServletHolder("userInfo", UserInfoServlet()) + + companion object { + val log = LoggerFactory.getLogger(ApplicationBase::class.java) + val spinner = + """
Loading...
""" + + fun getMimeType(filename: String): String = + when { + filename.endsWith(".html") -> "text/html" + filename.endsWith(".json") -> "application/json" + filename.endsWith(".js") -> "application/json" + filename.endsWith(".png") -> "image/png" + filename.endsWith(".jpg") -> "image/jpeg" + filename.endsWith(".jpeg") -> "image/jpeg" + filename.endsWith(".gif") -> "image/gif" + filename.endsWith(".svg") -> "image/svg+xml" + filename.endsWith(".css") -> "text/css" + else -> "text/plain" + } + } + +} + diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/AsyncApplicationWrapper.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/AsyncApplicationWrapper.kt new file mode 100644 index 00000000..a88a26b0 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/AsyncApplicationWrapper.kt @@ -0,0 +1,55 @@ +package com.simiacryptus.skyenet.sessions + +import java.util.concurrent.Semaphore +import java.util.function.Consumer + +class AsyncApplicationWrapper( + val parent: ChatApplicationBase, + sessionId: String, + sessionDataStorage: SessionDataStorage = parent.sessionDataStorage +) : PersistentSessionBase( + sessionId = sessionId, + sessionDataStorage = sessionDataStorage +) { + private val playSempaphores = mutableMapOf() + private val threads = mutableMapOf() + private val regenTriggers = mutableMapOf>() + private val linkTriggers = mutableMapOf>() + private val txtTriggers = mutableMapOf>() + private val session: PersistentSessionBase = this + + override fun run(userMessage: String, socket: MessageWebSocket) { + val operationID = ChatSession.randomID() + val sessionDiv = newSessionDiv(operationID, ApplicationBase.spinner) + val thread = Thread { + playSempaphores[operationID] = Semaphore(0) + try { + parent.processMessage(sessionId, userMessage, session, sessionDiv, socket) + } catch (e: Throwable) { + e.printStackTrace() + } finally { + } + } + threads[operationID] = thread + thread.start() + } + + override fun onCmd(id: String, code: String, socket: MessageWebSocket) { + if (code == "run") { + playSempaphores[id]?.release() + } + if (code == "cancel") { + threads[id]?.interrupt() + } + if (code == "regen") { + regenTriggers[id]?.accept(Unit) + } + if (code.startsWith("link")) { + linkTriggers[id]?.accept(Unit) + } + if (code.startsWith("userTxt,")) { + txtTriggers[id]?.accept(code.substring("userTxt,".length)) + } + super.onCmd(id, code, socket) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatApplicationBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatApplicationBase.kt new file mode 100644 index 00000000..8091aebb --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatApplicationBase.kt @@ -0,0 +1,32 @@ +package com.simiacryptus.skyenet.sessions + +abstract class ChatApplicationBase( + applicationName: String, + oauthConfig: String? = null, + temperature: Double = 0.1, +) : ApplicationBase( + applicationName = applicationName, + oauthConfig = oauthConfig, + temperature = temperature, +) { + + override fun newSession(sessionId: String): SessionInterface { + val handler = MutableSessionHandler(null) + handler.setDelegate(AsyncApplicationWrapper(this,sessionId)) + return handler + } + + abstract fun processMessage( + sessionId: String, + userMessage: String, + session: PersistentSessionBase, + sessionDiv: SessionDiv, + socket: MessageWebSocket + ) + + companion object { + val log = org.slf4j.LoggerFactory.getLogger(ChatApplicationBase::class.java) + } + +} + diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatSession.kt similarity index 69% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSession.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatSession.kt index 0b388afb..e930b1c0 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ChatSession.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/ChatSession.kt @@ -1,30 +1,36 @@ -package com.simiacryptus.skyenet.body +package com.simiacryptus.skyenet.sessions import com.simiacryptus.openai.OpenAIClient -abstract class ChatSession( - val parent: SkyenetSessionServerBase, +open class ChatSession( + val parent: ApplicationBase, sessionId: String, var model: OpenAIClient.Model = OpenAIClient.Models.GPT35Turbo, private var visiblePrompt: String, private var hiddenPrompt: String, private var systemPrompt: String, + val api: OpenAIClient, ) : PersistentSessionBase(sessionId, parent.sessionDataStorage) { init { - if (visiblePrompt.isNotBlank()) send("""aaa,
${visiblePrompt}
""") + if (visiblePrompt.isNotBlank()) { + send("""aaa,
${visiblePrompt}
""") + } } - open val messages = listOf( - OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.system, systemPrompt), - OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.assistant, hiddenPrompt), - ).toMutableList() + protected val messages by lazy { + val list = listOf( + OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.system, systemPrompt), + ).toMutableList() + if(hiddenPrompt.isNotBlank()) list += OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.assistant, hiddenPrompt) + list + } @Synchronized - override fun run(userMessage: String) { + override fun run(userMessage: String, socket: MessageWebSocket) { var responseContents = divInitializer() responseContents += """
$userMessage
""" - send("""$responseContents
${SkyenetSessionServerBase.spinner}
""") + send("""$responseContents
${ApplicationBase.spinner}
""") val response = handleMessage(userMessage, responseContents) if(null != response) { responseContents += """
${renderResponse(response)}
""" @@ -40,7 +46,7 @@ abstract class ChatSession( return response } - open fun getResponse() = parent.api.chat(newChatRequest, model).choices.first().message?.content.orEmpty() + open fun getResponse() = api.chat(newChatRequest, model).choices.first().message?.content.orEmpty() open fun renderResponse(response: String) = """
$response
""" diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MessageWebSocket.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MessageWebSocket.kt new file mode 100644 index 00000000..93cb3888 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MessageWebSocket.kt @@ -0,0 +1,72 @@ +package com.simiacryptus.skyenet.sessions + +import com.simiacryptus.openai.OpenAIClient +import com.simiacryptus.skyenet.servlet.AuthenticatedWebsite +import com.simiacryptus.skyenet.servlet.UsageServlet.Companion.incrementUsage +import com.simiacryptus.skyenet.servlet.UserSettingsServlet.Companion.getUserSettings +import org.eclipse.jetty.websocket.api.Session +import org.eclipse.jetty.websocket.api.WebSocketAdapter +import org.slf4j.event.Level + +class MessageWebSocket( + val sessionId: String, + private val sessionState: SessionInterface, + val authId: String?, + val sessionDataStorage: SessionDataStorage, +) : WebSocketAdapter() { + + val api: OpenAIClient + get() { + val userinfo = AuthenticatedWebsite.users[authId] + val userApi: OpenAIClient? = if (userinfo == null) null else { + val userSettings = getUserSettings(userinfo) + if (userSettings.apiKey.isBlank()) null else object : OpenAIClient( + key = userSettings.apiKey, + logLevel = Level.DEBUG, + logStreams = mutableListOf( + sessionDataStorage.getSessionDir(sessionId).resolve("openai.log").outputStream().buffered() + ), + ) { + override fun incrementTokens(model: Model?, tokens: Int) { + incrementUsage(sessionId, userinfo, model!!, tokens) + super.incrementTokens(model, tokens) + } + } + } + return userApi ?: object : OpenAIClient( + logLevel = Level.DEBUG, + logStreams = mutableListOf( + sessionDataStorage.getSessionDir(sessionId).resolve("openai.log").outputStream().buffered() + ) + ) { + override fun incrementTokens(model: Model?, tokens: Int) { + incrementUsage(sessionId, userinfo, model!!, tokens) + super.incrementTokens(model, tokens) + } + } + } + + override fun onWebSocketConnect(session: Session) { + super.onWebSocketConnect(session) + WebSocketServer.log.debug("{} - Socket connected: {}", sessionId, session.remote) + sessionState.addSocket(this) + sessionState.getReplay().forEach { + try { + remote.sendString(it) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + override fun onWebSocketText(message: String) { + super.onWebSocketText(message) + sessionState.onWebSocketText(this, message) + } + + override fun onWebSocketClose(statusCode: Int, reason: String?) { + super.onWebSocketClose(statusCode, reason) + sessionState.removeSocket(this) + } + +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/MutableSessionHandler.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MutableSessionHandler.kt similarity index 68% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/body/MutableSessionHandler.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MutableSessionHandler.kt index a194cf7e..b27d1145 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/MutableSessionHandler.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/MutableSessionHandler.kt @@ -1,32 +1,32 @@ -package com.simiacryptus.skyenet.body - -class MutableSessionHandler(initialDelegate: SessionInterface?) : SessionInterface { - private var priorDelegates: MutableList = mutableListOf() - private var currentDelegate: SessionInterface? = initialDelegate - - fun setDelegate(delegate: SessionInterface) { - if(null != currentDelegate) priorDelegates.add(currentDelegate!!) - currentDelegate = delegate - for (socket in sockets) { - currentDelegate!!.addSocket(socket) - } - } - - private val sockets: MutableSet = mutableSetOf() - - override fun removeSocket(socket: WebSocketServer.MessageWebSocket) { - sockets.remove(socket) - currentDelegate?.removeSocket(socket) - } - - override fun addSocket(socket: WebSocketServer.MessageWebSocket) { - sockets.add(socket) - currentDelegate?.addSocket(socket) - } - override fun getReplay(): List = - (priorDelegates.flatMap { it.getReplay() } + (currentDelegate?.getReplay() ?: listOf())) - - override fun onWebSocketText(socket: WebSocketServer.MessageWebSocket, message: String) { - currentDelegate?.onWebSocketText(socket, message) - } +package com.simiacryptus.skyenet.sessions + +class MutableSessionHandler(initialDelegate: SessionInterface?) : SessionInterface { + private var priorDelegates: MutableList = mutableListOf() + private var currentDelegate: SessionInterface? = initialDelegate + + fun setDelegate(delegate: SessionInterface) { + if(null != currentDelegate) priorDelegates.add(currentDelegate!!) + currentDelegate = delegate + for (socket in sockets) { + currentDelegate!!.addSocket(socket) + } + } + + private val sockets: MutableSet = mutableSetOf() + + override fun removeSocket(socket: MessageWebSocket) { + sockets.remove(socket) + currentDelegate?.removeSocket(socket) + } + + override fun addSocket(socket: MessageWebSocket) { + sockets.add(socket) + currentDelegate?.addSocket(socket) + } + override fun getReplay(): List = + (priorDelegates.flatMap { it.getReplay() } + (currentDelegate?.getReplay() ?: listOf())) + + override fun onWebSocketText(socket: MessageWebSocket, message: String) { + currentDelegate?.onWebSocketText(socket, message) + } } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/PersistentSessionBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/PersistentSessionBase.kt similarity index 73% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/body/PersistentSessionBase.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/PersistentSessionBase.kt index e7942d6a..ecde1498 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/PersistentSessionBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/PersistentSessionBase.kt @@ -1,87 +1,88 @@ -package com.simiacryptus.skyenet.body - -import com.google.common.util.concurrent.MoreExecutors -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger - -abstract class PersistentSessionBase( - sessionId: String, - private val sessionDataStorage: SessionDataStorage, - private val messageStates: LinkedHashMap = sessionDataStorage.loadMessages(sessionId), -) : SessionBase(sessionId) { - - private val messageVersions = HashMap() - - companion object { - val logger = org.slf4j.LoggerFactory.getLogger(WebSocketServer::class.java) - } - - public override fun send(out: String) { - try { - logger.debug("$sessionId - $out") - val split = out.split(',', ignoreCase = false, limit = 2) - val newVersion = setMessage(split[0], split[1]) - publish("${split[0]},$newVersion,${split[1]}") - } catch (e: Exception) { - logger.debug("$sessionId - $out", e) - } - } - - protected open fun setMessage(key: String, value: String): Int { - if (messageStates.containsKey(key) && messageStates[key] == value) return -1 - sessionDataStorage.updateMessage(sessionId, key, value) - messageStates.put(key, value) - return messageVersions.computeIfAbsent(key) { AtomicInteger(0) }.incrementAndGet() - } - - override fun getReplay(): List { - return messageStates.entries.map { - "${it.key},${ - messageVersions.computeIfAbsent(it.key) { AtomicInteger(1) }.get() - },${it.value}" - } - } - - - open val pool = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()) - - override fun onWebSocketText(socket: WebSocketServer.MessageWebSocket, message: String) { - pool.submit { - logger.debug("$sessionId - Received message: $message") - try { - val opCmdPattern = """![a-z]{3,7},.*""".toRegex() - if (opCmdPattern.matches(message)) { - val id = message.substring(1, message.indexOf(",")) - val code = message.substring(id.length + 2) - onCmd(id, code) - } else { - onRun(message) - } - } catch (e: Exception) { - logger.warn("$sessionId - Error processing message: $message", e) - } - } - } - - protected open fun onRun(describedInstruction: String) { - Thread { - try { - run(describedInstruction) - } catch (e: Exception) { - logger.warn( - "$sessionId - Error processing message: $describedInstruction", - e - ) - } - }.start() - } - - protected open fun onCmd(id: String, code: String) { - } - - protected abstract fun run( - userMessage: String, - ) - - +package com.simiacryptus.skyenet.sessions + +import com.google.common.util.concurrent.MoreExecutors +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger + +abstract class PersistentSessionBase( + sessionId: String, + private val sessionDataStorage: SessionDataStorage, + private val messageStates: LinkedHashMap = sessionDataStorage.loadMessages(sessionId), +) : SessionBase(sessionId) { + + private val messageVersions = HashMap() + + companion object { + val log = org.slf4j.LoggerFactory.getLogger(WebSocketServer::class.java) + } + + public override fun send(out: String) { + try { + log.debug("Send Msg: $sessionId - $out") + val split = out.split(',', ignoreCase = false, limit = 2) + val newVersion = setMessage(split[0], split[1]) + publish("${split[0]},$newVersion,${split[1]}") + } catch (e: Exception) { + log.debug("$sessionId - $out", e) + } + } + + protected open fun setMessage(key: String, value: String): Int { + if (messageStates.containsKey(key) && messageStates[key] == value) return -1 + sessionDataStorage.updateMessage(sessionId, key, value) + messageStates.put(key, value) + return messageVersions.computeIfAbsent(key) { AtomicInteger(0) }.incrementAndGet() + } + + override fun getReplay(): List { + return messageStates.entries.map { + "${it.key},${ + messageVersions.computeIfAbsent(it.key) { AtomicInteger(1) }.get() + },${it.value}" + } + } + + + open val pool = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()) + + override fun onWebSocketText(socket: MessageWebSocket, message: String) { + pool.submit { + log.debug("$sessionId - Received message: $message") + try { + val opCmdPattern = """![a-z]{3,7},.*""".toRegex() + if (opCmdPattern.matches(message)) { + val id = message.substring(1, message.indexOf(",")) + val code = message.substring(id.length + 2) + onCmd(id, code, socket) + } else { + onRun(message, socket) + } + } catch (e: Exception) { + log.warn("$sessionId - Error processing message: $message", e) + } + } + } + + protected open fun onRun(describedInstruction: String, socket: MessageWebSocket) { + Thread { + try { + run(describedInstruction, socket) + } catch (e: Exception) { + log.warn( + "$sessionId - Error processing message: $describedInstruction", + e + ) + } + }.start() + } + + protected open fun onCmd(id: String, code: String, socket: MessageWebSocket) { + } + + protected abstract fun run( + userMessage: String, + socket: MessageWebSocket, + ) + + } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionBase.kt similarity index 82% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionBase.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionBase.kt index d98548f0..17f13e4c 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/SessionBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionBase.kt @@ -1,59 +1,59 @@ -package com.simiacryptus.skyenet.body - -abstract class SessionBase(val sessionId: String) : SessionInterface { - private val sockets: MutableSet = mutableSetOf() - - override fun removeSocket(socket: WebSocketServer.MessageWebSocket) { - sockets.remove(socket) - } - - override fun addSocket(socket: WebSocketServer.MessageWebSocket) { - sockets.add(socket) - } - protected fun publish( - out: String, - ) { - val socketsSnapshot = sockets.toTypedArray() - socketsSnapshot.forEach { - try { - it.remote.sendString(out) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - private val sentMessages: MutableList = mutableListOf() - - protected open fun send(out: String) { - sentMessages.add(out) - publish(out) - } - - override fun getReplay(): List { - return sentMessages - } - - fun newSessionDiv( - operationID: String, spinner: String - ): SessionDiv { - var responseContents = ChatSession.divInitializer(operationID) - send(responseContents) - return object : SessionDiv() { - override fun append(htmlToAppend: String, showSpinner: Boolean) { - if (htmlToAppend.isNotBlank()) { - responseContents += """
$htmlToAppend
""" - } - val spinner1 = if (showSpinner) """
$spinner
""" else "" - return this@SessionBase.send("""$responseContents$spinner1""") - } - - override fun sessionID(): String { - return sessionId - } - - override fun divID(): String { - return operationID - } - } - } +package com.simiacryptus.skyenet.sessions + +abstract class SessionBase(val sessionId: String) : SessionInterface { + private val sockets: MutableSet = mutableSetOf() + + override fun removeSocket(socket: MessageWebSocket) { + sockets.remove(socket) + } + + override fun addSocket(socket: MessageWebSocket) { + sockets.add(socket) + } + protected fun publish( + out: String, + ) { + val socketsSnapshot = sockets.toTypedArray() + socketsSnapshot.forEach { + try { + it.remote.sendString(out) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + private val sentMessages: MutableList = mutableListOf() + + protected open fun send(out: String) { + sentMessages.add(out) + publish(out) + } + + override fun getReplay(): List { + return sentMessages + } + + fun newSessionDiv( + operationID: String, spinner: String + ): SessionDiv { + var responseContents = ChatSession.divInitializer(operationID) + send(responseContents) + return object : SessionDiv() { + override fun append(htmlToAppend: String, showSpinner: Boolean) { + if (htmlToAppend.isNotBlank()) { + responseContents += """
$htmlToAppend
""" + } + val spinner1 = if (showSpinner) """
$spinner
""" else "" + return this@SessionBase.send("""$responseContents$spinner1""") + } + + override fun sessionID(): String { + return sessionId + } + + override fun divID(): String { + return operationID + } + } + } } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt new file mode 100644 index 00000000..d832a999 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt @@ -0,0 +1,161 @@ +package com.simiacryptus.skyenet.sessions + +import com.simiacryptus.util.JsonUtil +import java.io.File +import java.util.* +import kotlin.collections.LinkedHashMap + +open class SessionDataStorage( + val dataDir: File = File("sessionData") +) { + + open fun updateMessage(sessionId: String, messageId: String, value: String) { + validateSessionId(sessionId) + val file = File(getMessageDir(sessionId), "$messageId.json") + log.debug("Updating message for $sessionId / $messageId: ${file.absolutePath}") + file.parentFile.mkdirs() + JsonUtil.objectMapper().writeValue(file, value) + } + + open fun loadMessages(sessionId: String): LinkedHashMap { + validateSessionId(sessionId) + val messageDir = getMessageDir(sessionId) + val messages = LinkedHashMap() + log.debug("Loading messages for $sessionId: ${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.size} messages for $sessionId") + return messages + } + + protected open fun getMessageDir(sessionId: String): File { + validateSessionId(sessionId) + val sessionDir = getInstanceDir(sessionId) + val messageDir = File(sessionDir, "messages") + log.debug("Message Dir for $sessionId: ${messageDir.absolutePath}") + return messageDir + } + + open fun listSessions(): List { + val files = dataDir.listFiles()?.flatMap { it.listFiles()?.toList() ?: listOf() }?.filter { sessionDir -> + val operationDir = File(sessionDir, "messages") + if (!operationDir.exists()) false else { + val listFiles = operationDir.listFiles().filter { it.isFile && !it.name.startsWith("aaa") } + (listFiles?.size ?: 0) > 0 + } + } + log.debug("Sessions: {}", files?.map { it.parentFile.name + "-" + it.name }) + return files?.map { it.parentFile.name + "-" + it.name } ?: listOf() + } + + open fun getSessionName(sessionId: String): String { + val userMessages = getUserMessages(sessionId) + if (userMessages.size > 0) { + val first = userMessages.first() + log.debug("Session $sessionId: ${first}") + return first + } else { + log.info("Session $sessionId: No messages") + return sessionId + } + } + + @Suppress("MemberVisibilityCanBePrivate", "Unused") + fun getUserMessages(sessionId: String): List { + validateSessionId(sessionId) + val userMessages = getMessageDir(sessionId).listFiles()?.filter { file -> + file.isFile + }?.sortedBy { file -> + file.lastModified() + //JsonUtil.objectMapper().readValue(file, OperationStatus::class.java).created + }?.map { messageFile -> + val fileText = messageFile.readText() + val split = fileText.split("

") + if (split.size < 2) { + log.info("Session $sessionId: No messages") + "" + } else { + val stringList = split[1].split("

") + if (stringList.isEmpty()) { + log.info("Session $sessionId: No messages") + "" + } else { + stringList.first() + } + } + }?.filter { it.isNotEmpty() } ?: listOf() + return userMessages + } + + protected open fun getInstanceDir(sessionId: String): File { + validateSessionId(sessionId) + val sessionDir = File(getDateDir(sessionId), getInstanceId(sessionId)) + log.debug("Instance Dir for $sessionId: ${sessionDir.absolutePath}") + return sessionDir + } + + open fun getSessionDir(sessionId: String) = getInstanceDir(sessionId) + + protected open fun getDateDir(sessionId: String): File { + validateSessionId(sessionId) + val sessionGroupDir = File(dataDir, getDate(sessionId)) + log.debug("Date Dir for $sessionId: ${sessionGroupDir.absolutePath}") + return sessionGroupDir + } + + protected open fun getDate(sessionId: String): String { + validateSessionId(sessionId) + return sessionId.split("-").firstOrNull() ?: sessionId + } + + protected open fun getInstanceId(sessionId: String): String { + validateSessionId(sessionId) + return stripPrefix(stripPrefix(sessionId, getDate(sessionId)), "-") + } + + fun getSettings(sessionId: String, clazz: Class): T? { + validateSessionId(sessionId) + val settingsFile = File(getSessionDir(sessionId), "settings.json") + return if (!settingsFile.exists()) null else { + JsonUtil.objectMapper().readValue(settingsFile, clazz) as T + } + } + + fun updateSettings(sessionId: String, settings: T): T { + validateSessionId(sessionId) + val settingsFile = File(getSessionDir(sessionId), "settings.json") + settingsFile.parentFile.mkdirs() + JsonUtil.objectMapper().writeValue(settingsFile, settings) + return settings + } + + + companion object { + fun stripPrefix(text: String, prefix: String): String { + val startsWith = text.startsWith(prefix) + return if (startsWith) { + text.substring(prefix.length) + } else { + text + } + } + + fun newID(): String { + val uuid = UUID.randomUUID().toString().split("-").first() + val yyyyMMdd = java.time.LocalDate.now().toString().replace("-", "") + log.debug("New ID: $yyyyMMdd-$uuid") + return "$yyyyMMdd-$uuid" + } + + fun validateSessionId(sessionId: String) { + if (!sessionId.matches("""\d{8}-\w{8}""".toRegex())) { + throw IllegalArgumentException("Invalid session ID: $sessionId") + } + } + + private val log = org.slf4j.LoggerFactory.getLogger(SessionDataStorage::class.java) + + } +} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDiv.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDiv.kt new file mode 100644 index 00000000..e815806e --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDiv.kt @@ -0,0 +1,7 @@ +package com.simiacryptus.skyenet.sessions + +abstract class SessionDiv { + abstract fun append(htmlToAppend: String, showSpinner: Boolean) : Unit + abstract fun sessionID(): String + abstract fun divID(): String +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionInterface.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionInterface.kt new file mode 100644 index 00000000..fbece6cc --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionInterface.kt @@ -0,0 +1,8 @@ +package com.simiacryptus.skyenet.sessions + +interface SessionInterface { + fun removeSocket(socket: MessageWebSocket) + fun addSocket(socket: MessageWebSocket) + fun getReplay(): List + fun onWebSocketText(socket: MessageWebSocket, message: String) +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/WebSocketServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/WebSocketServer.kt new file mode 100644 index 00000000..2d80f053 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/WebSocketServer.kt @@ -0,0 +1,53 @@ +package com.simiacryptus.skyenet.sessions + +import com.simiacryptus.skyenet.servlet.NewSessionServlet +import org.eclipse.jetty.servlet.DefaultServlet +import org.eclipse.jetty.servlet.ServletHolder +import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.webapp.WebAppContext +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory + +abstract class WebSocketServer(val resourceBase: String) { + + abstract val applicationName: String + abstract val sessionDataStorage: SessionDataStorage + + val stateCache: MutableMap = mutableMapOf() + + inner class WebSocketHandler : JettyWebSocketServlet() { + override fun configure(factory: JettyWebSocketServletFactory) { + factory.setCreator { req, resp -> + val sessionId = req.parameterMap["sessionId"]?.firstOrNull() + val authId = req.cookies?.find { it.name == "sessionId" }?.value + return@setCreator if (null == sessionId) { + null + } else { + MessageWebSocket(sessionId, stateCache.getOrPut(sessionId) { + newSession(sessionId) + }, authId, sessionDataStorage) + } + } + } + } + + abstract fun newSession(sessionId: String): SessionInterface + + open val baseResource: Resource? get() = Resource.newResource(javaClass.classLoader.getResource(resourceBase)) + protected val newSessionServlet by lazy { NewSessionServlet() } + protected val webSocketHandler by lazy { WebSocketHandler() } + protected val defaultServlet by lazy { DefaultServlet() } + + open fun configure(webAppContext: WebAppContext, path: String = "/", baseUrl: String) { + webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/default", defaultServlet), "/") + webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/ws", webSocketHandler), "/ws/*") + webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/newSession", newSessionServlet),"/newSession") + } + + + companion object { + val log = org.slf4j.LoggerFactory.getLogger(WebSocketServer::class.java) + } +} + + diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ClasspathResource.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/ClasspathResource.kt similarity index 95% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/body/ClasspathResource.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/util/ClasspathResource.kt index 8e20e851..355f8ec0 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/body/ClasspathResource.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/ClasspathResource.kt @@ -1,116 +1,116 @@ -package com.simiacryptus.skyenet.body - -import org.eclipse.jetty.util.URIUtil -import org.eclipse.jetty.util.resource.Resource -import org.slf4j.LoggerFactory -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.net.MalformedURLException -import java.net.URI -import java.net.URISyntaxException -import java.net.URL -import java.nio.channels.ReadableByteChannel - -class ClasspathResource( - val url: URL, - @Transient private var useCaches: Boolean = __defaultUseCaches, -) : Resource() { - private val classLoader: ClassLoader = this@ClasspathResource.javaClass.classLoader - val path = url.path.split("jar!/").last() - - override fun close() { - } - - override fun exists(): Boolean { - val resource = classLoader.getResourceAsStream(path) - if (resource == null) { - LOG.info("Resource not found: {}", path) - return false - } - resource.close() - return true - } - - override fun isDirectory(): Boolean { - return exists() && url.toExternalForm().endsWith("/") - } - - override fun lastModified(): Long { - return -1 - } - - override fun length(): Long { - return inputStream?.readAllBytes()?.size?.toLong() ?: -1L - } - - override fun getURI(): URI { - return try { - url.toURI() - } catch (e: URISyntaxException) { - throw RuntimeException(e) - } - } - - @Throws(IOException::class) - override fun getFile(): File? { - return null - } - - override fun getName(): String { - return url.toExternalForm() - } - - @Throws(IOException::class) - override fun getInputStream(): InputStream? { - return classLoader.getResourceAsStream(path) - } - - @Throws(IOException::class) - override fun getReadableByteChannel(): ReadableByteChannel? { - return null - } - - @Throws(SecurityException::class) - override fun delete(): Boolean { - throw SecurityException("Delete not supported") - } - - @Throws(SecurityException::class) - override fun renameTo(dest: Resource): Boolean { - throw SecurityException("RenameTo not supported") - } - - override fun list(): Array? { - return null - } - - @Throws(IOException::class) - override fun addPath(path: String): Resource { - if (URIUtil.canonicalPath(path) == null) - throw MalformedURLException(path) - val newUrl = URL(URIUtil.addEncodedPaths(url.toExternalForm(), URIUtil.encodePath(path))) - return ClasspathResource(newUrl, useCaches = useCaches) - } - - override fun toString(): String { - return url.toExternalForm() - } - - override fun hashCode(): Int { - return url.toExternalForm().hashCode() - } - - override fun equals(other: Any?): Boolean { - return other is ClasspathResource && url.toExternalForm() == other.url.toExternalForm() - } - - @Throws(MalformedURLException::class) - override fun isContainedIn(containingResource: Resource): Boolean { - return false - } - - companion object { - private val LOG = LoggerFactory.getLogger(ClasspathResource::class.java) - } +package com.simiacryptus.skyenet.util + +import org.eclipse.jetty.util.URIUtil +import org.eclipse.jetty.util.resource.Resource +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.MalformedURLException +import java.net.URI +import java.net.URISyntaxException +import java.net.URL +import java.nio.channels.ReadableByteChannel + +class ClasspathResource( + val url: URL, + @Transient private var useCaches: Boolean = __defaultUseCaches, +) : Resource() { + private val classLoader: ClassLoader = this@ClasspathResource.javaClass.classLoader + val path = url.path.split("jar!/").last() + + override fun close() { + } + + override fun exists(): Boolean { + val resource = classLoader.getResourceAsStream(path) + if (resource == null) { + LOG.info("Resource not found: {}", path) + return false + } + resource.close() + return true + } + + override fun isDirectory(): Boolean { + return exists() && url.toExternalForm().endsWith("/") + } + + override fun lastModified(): Long { + return -1 + } + + override fun length(): Long { + return inputStream?.readAllBytes()?.size?.toLong() ?: -1L + } + + override fun getURI(): URI { + return try { + url.toURI() + } catch (e: URISyntaxException) { + throw RuntimeException(e) + } + } + + @Throws(IOException::class) + override fun getFile(): File? { + return null + } + + override fun getName(): String { + return url.toExternalForm() + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream? { + return classLoader.getResourceAsStream(path) + } + + @Throws(IOException::class) + override fun getReadableByteChannel(): ReadableByteChannel? { + return null + } + + @Throws(SecurityException::class) + override fun delete(): Boolean { + throw SecurityException("Delete not supported") + } + + @Throws(SecurityException::class) + override fun renameTo(dest: Resource): Boolean { + throw SecurityException("RenameTo not supported") + } + + override fun list(): Array? { + return null + } + + @Throws(IOException::class) + override fun addPath(path: String): Resource { + if (URIUtil.canonicalPath(path) == null) + throw MalformedURLException(path) + val newUrl = URL(URIUtil.addEncodedPaths(url.toExternalForm(), URIUtil.encodePath(path))) + return ClasspathResource(newUrl, useCaches = useCaches) + } + + override fun toString(): String { + return url.toExternalForm() + } + + override fun hashCode(): Int { + return url.toExternalForm().hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is ClasspathResource && url.toExternalForm() == other.url.toExternalForm() + } + + @Throws(MalformedURLException::class) + override fun isContainedIn(containingResource: Resource): Boolean { + return false + } + + companion object { + private val LOG = LoggerFactory.getLogger(ClasspathResource::class.java) + } } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt new file mode 100644 index 00000000..8678e6d5 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt @@ -0,0 +1,29 @@ +package com.simiacryptus.skyenet.util + +import com.simiacryptus.skyenet.sessions.ApplicationBase +import com.simiacryptus.skyenet.sessions.ChatSession + +class HtmlTools(private val operationID: String = ChatSession.randomID()) { + val spinner: String get() = """
${ApplicationBase.spinner}
""" + val playButton: String get() = """""" + val cancelButton: String get() = """""" + val regenButton: String get() = """""" + + val linkTriggers = mutableMapOf>() + + private val txtTriggers = mutableMapOf>() + fun hrefLink(handler: java.util.function.Consumer): String { + val operationID = ChatSession.randomID() + linkTriggers[operationID] = handler + return """""" + } + fun textInput(handler: java.util.function.Consumer): String { + val operationID = ChatSession.randomID() + txtTriggers[operationID] = handler + //language=HTML + return """
+ + +
""".trimIndent() + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt new file mode 100644 index 00000000..5686f0b4 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt @@ -0,0 +1,21 @@ +package com.simiacryptus.skyenet.util + +import com.vladsch.flexmark.ext.tables.TablesExtension +import com.vladsch.flexmark.html.HtmlRenderer +import com.vladsch.flexmark.parser.Parser +import com.vladsch.flexmark.util.data.MutableDataSet + +object MarkdownUtil { + fun renderMarkdown(response: String, options: MutableDataSet = defaultOptions()): String { + val parser = Parser.builder(options).build() + val renderer = HtmlRenderer.builder(options).build() + val document = parser.parse(response) + return renderer.render(document) + } + + private fun defaultOptions(): MutableDataSet { + val options = MutableDataSet() + options.set(Parser.EXTENSIONS, listOf(TablesExtension.create())) + return options + } +} \ No newline at end of file diff --git a/webui/src/main/resources/readOnly/chat.css b/webui/src/main/resources/readOnly/chat.css new file mode 100644 index 00000000..02cac400 --- /dev/null +++ b/webui/src/main/resources/readOnly/chat.css @@ -0,0 +1,228 @@ +#messages { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 40px; + overflow-y: auto; + padding: 10px; + padding-top: 40px; + flex-grow: 1; + flex-shrink: 1; +} + +.message { + background-color: #f0f0f0; + border-radius: 5px; + padding: 10px; + margin-bottom: 10px; + overflow: scroll; + z-index: 100; +} + +.reply-form { + width: 100%; +} + +.chat-input { + resize: vertical; +} + +.reply-input { + resize: vertical; +} + +.href-link { + text-decoration: underline; + color: blue; +} + +#message { + flex-grow: 1; + margin-right: 5px; +} + +.spinner-border { + display: block; + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +#toolbar { + background-color: #f1f1f1; + padding: 5px; + position: fixed; + top: 0; + width: 100%; + horiz-align: left; + z-index: 1; +} + +#toolbar a { + color: #000; + text-decoration: none; + padding: 5px; +} + +#toolbar a:hover { + background-color: #ddd; +} + + +#namebar { + background-color: #f1f1f1; + padding: 5px; + position: fixed; + top: 0; + right: 0; + horiz-align: right; + z-index: 2; +} + +#namebar a { + color: #000; + text-decoration: none; + padding: 5px; +} + +#namebar a:hover { + background-color: #ddd; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 80%; + position: relative; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +pre { + white-space: pre-wrap; /* Since CSS modules do not automatically apply this */ +} + +#container { + display: flex; + flex-direction: column; + height: 100vh; /* Adjust this value based on your preferred container height */ +} + +.play-button { + font-size: 48px; + font-weight: bold; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; +} + +.play-button:focus { + outline: none; +} + +.play-button:active { + transform: scale(0.9); +} + + +.regen-button { + font-size: 48px; + font-weight: bold; + color: chartreuse; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; +} + +.regen-button:focus { + outline: none; +} + +.regen-button:active { + transform: scale(0.9); +} + +.message-container { + position: relative; /* Add this line to set the position property of the parent div */ +} + +.cancel-button { + position: absolute; + top: 8px; + right: 8px; + font-size: 16px; + font-weight: bold; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; +} + +.cancel-button:focus { + outline: none; +} + +.cancel-button:active { + transform: scale(0.9); +} + +.collapsible-content { + overflow: hidden; + max-height: 0; + transition: max-height 0.2s ease-out; +} diff --git a/webui/src/main/resources/readOnly/chat.js b/webui/src/main/resources/readOnly/chat.js new file mode 100644 index 00000000..f946c020 --- /dev/null +++ b/webui/src/main/resources/readOnly/chat.js @@ -0,0 +1,65 @@ + +let socket; + +function getSessionId() { + if (!window.location.hash) { + fetch('newSession') + .then(response => { + if (response.ok) { + return response.text(); + } else { + throw new Error('Failed to get new session ID'); + } + }) + .then(sessionId => { + window.location.hash = sessionId; + connect(sessionId); + }); + } else { + return window.location.hash.substring(1); + } +} + +function send(message) { + console.log('Sending message:', message); + socket.send(message); +} + +function connect(sessionId, customReceiveFunction) { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.hostname; + const port = window.location.port; + let path = window.location.pathname; + let strings = path.split('/'); + if(strings.length >= 2 && strings[1] !== '' && strings[1] !== 'index.html') { + path = '/' + strings[1] + '/'; + } else { + path = '/'; + } + + socket = new WebSocket(`${protocol}//${host}:${port}${path}ws?sessionId=${sessionId}`); + + socket.addEventListener('open', (event) => { + console.log('WebSocket connected:', event); + showDisconnectedOverlay(false); + }); + + socket.addEventListener('message', customReceiveFunction || onWebSocketText); + + socket.addEventListener('close', (event) => { + console.log('WebSocket closed:', event); + showDisconnectedOverlay(true); + setTimeout(() => { + connect(getSessionId(), customReceiveFunction); + }, 3000); + }); + + socket.addEventListener('error', (event) => { + console.error('WebSocket error:', event); + }); +} + +function showDisconnectedOverlay(show) { + const overlay = document.getElementById('disconnected-overlay'); + overlay.style.display = show ? 'block' : 'none'; +} diff --git a/webui/src/main/resources/readOnly/favicon.svg b/webui/src/main/resources/readOnly/favicon.svg new file mode 100644 index 00000000..2cbd9fdb --- /dev/null +++ b/webui/src/main/resources/readOnly/favicon.svg @@ -0,0 +1,724 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webui/src/main/resources/readOnly/index.html b/webui/src/main/resources/readOnly/index.html new file mode 100644 index 00000000..aeb8b50a --- /dev/null +++ b/webui/src/main/resources/readOnly/index.html @@ -0,0 +1,45 @@ + + + + + WebSocket Client + + + + + + + + + + +
+ +
+ Login +
+ + + +
+
+
+ +
+

Disconnected. Attempting to reconnect...

+
+ + + + diff --git a/webui/src/main/resources/readOnly/main.js b/webui/src/main/resources/readOnly/main.js new file mode 100644 index 00000000..7629f48c --- /dev/null +++ b/webui/src/main/resources/readOnly/main.js @@ -0,0 +1,130 @@ +function showModal(endpoint) { + fetchData(endpoint); + document.getElementById('modal').style.display = 'block'; +} + +function closeModal() { + document.getElementById('modal').style.display = 'none'; +} + +async function fetchData(endpoint) { + try { + // Add session id to the endpoint as a path parameter + const sessionId = getSessionId(); + if (sessionId) { + endpoint = endpoint + "?sessionId=" + sessionId; + } + const response = await fetch(endpoint); + const text = await response.text(); + document.getElementById('modal-content').innerHTML = "
" + text + "
"; + Prism.highlightAll(); + } catch (error) { + console.error('Error fetching data:', error); + } +} + +let messageVersions = {}; + +function onWebSocketText(event) { + console.log('WebSocket message:', event); + const messagesDiv = document.getElementById('messages'); + + // Parse message e.g. "id,version,content" + const firstCommaIndex = event.data.indexOf(','); + const secondCommaIndex = event.data.indexOf(',', firstCommaIndex + 1); + const messageId = event.data.substring(0, firstCommaIndex); + const messageVersion = event.data.substring(firstCommaIndex + 1, secondCommaIndex); + const messageContent = event.data.substring(secondCommaIndex + 1); + // If messageVersion isn't more than the version for the messageId using the version map, then ignore the message + if (messageVersion <= (messageVersions[messageId] || 0)) { + console.log("Ignoring message with id " + messageId + " and version " + messageVersion); + return; + } else { + messageVersions[messageId] = messageVersion; + } + + let messageDiv = document.getElementById(messageId); + + if (messageDiv) { + messageDiv.innerHTML = messageContent; + } else { + messageDiv = document.createElement('div'); + messageDiv.className = 'message message-container'; // Add the message-container class + messageDiv.id = messageId; + messageDiv.innerHTML = messageContent; + messagesDiv.appendChild(messageDiv); + } + + messagesDiv.scrollTop = messagesDiv.scrollHeight; + Prism.highlightAll(); +} + +document.addEventListener('DOMContentLoaded', () => { + + document.getElementById('history').addEventListener('click', () => showModal('sessions')); + document.getElementById('settings').addEventListener('click', () => showModal('settings')); + document.querySelector('.close').addEventListener('click', closeModal); + + window.addEventListener('click', (event) => { + if (event.target === document.getElementById('modal')) { + closeModal(); + } + }); + + const messageInput = document.getElementById('message'); + const usage = document.getElementById('usage'); + + const sessionId = getSessionId(); + if (sessionId) { + connect(sessionId, onWebSocketText); + usage.href = '/usage/?sessionId=' + sessionId; + } else { + connect(undefined, onWebSocketText); + } + + document.getElementById("files").addEventListener("click", function (event) { + event.preventDefault(); // Prevent the default behavior of the anchor tag + const sessionId = getSessionId(); + const url = "fileIndex/" + sessionId + "/"; + window.open(url, "_blank"); // Open the URL in a new tab + }); + + const loginLink = document.getElementById('username'); + if (loginLink) { + loginLink.href = '/googleLogin?redirect=' + encodeURIComponent(window.location.pathname); + } + + fetch('appInfo') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.applicationName) { + document.title = data.applicationName; + } + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); + + fetch('userInfo') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.name && loginLink) { + loginLink.innerHTML = data.name; + loginLink.href = "/userSettings"; + } + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); +}); + diff --git a/webui/src/main/resources/simpleSession/chat.css b/webui/src/main/resources/simpleSession/chat.css index d454cdde..0162e987 100644 --- a/webui/src/main/resources/simpleSession/chat.css +++ b/webui/src/main/resources/simpleSession/chat.css @@ -129,6 +129,7 @@ position: fixed; top: 0; width: 100%; + horiz-align: left; z-index: 1; } @@ -142,6 +143,27 @@ background-color: #ddd; } + +#namebar { + background-color: #f1f1f1; + padding: 5px; + position: fixed; + top: 0; + right: 0; + horiz-align: right; + z-index: 2; +} + +#namebar a { + color: #000; + text-decoration: none; + padding: 5px; +} + +#namebar a:hover { + background-color: #ddd; +} + /* Modal */ .modal { display: none; diff --git a/webui/src/main/resources/simpleSession/favicon.png b/webui/src/main/resources/simpleSession/favicon.png deleted file mode 100644 index bec9d99a54d729f50a660141ca5a9f14181ea455..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126746 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ξ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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webui/src/main/resources/simpleSession/index.html b/webui/src/main/resources/simpleSession/index.html index 6e99a6fb..f96c6feb 100644 --- a/webui/src/main/resources/simpleSession/index.html +++ b/webui/src/main/resources/simpleSession/index.html @@ -7,23 +7,28 @@ - +

+ +
+ Login
diff --git a/webui/src/main/resources/simpleSession/main.js b/webui/src/main/resources/simpleSession/main.js index b58c803f..2eb82a5e 100644 --- a/webui/src/main/resources/simpleSession/main.js +++ b/webui/src/main/resources/simpleSession/main.js @@ -9,9 +9,14 @@ function closeModal() { async function fetchData(endpoint) { try { + // Add session id to the endpoint as a path parameter + const sessionId = getSessionId(); + if (sessionId) { + endpoint = endpoint + "?sessionId=" + sessionId; + } const response = await fetch(endpoint); const text = await response.text(); - document.getElementById('modal-content').innerHTML = "
" + text + "
"; + document.getElementById('modal-content').innerHTML = "
" + text + "
"; Prism.highlightAll(); } catch (error) { console.error('Error fetching data:', error); @@ -52,55 +57,13 @@ function onWebSocketText(event) { messagesDiv.scrollTop = messagesDiv.scrollHeight; Prism.highlightAll(); - updateMessageDivs(); } -function updateMessageDivs() -{ - /* - console.log("Updating message divs"); - Get all message containers - var messageContainers = document.querySelectorAll('.message-container'); - // Loop through each message container - messageContainers.forEach(function (container) { - console.log("Updating message container: " + container); - // Get the first child which is a div (header) and the remaining children (content) - var header = container.children[0]; - while (header.tagName !== 'DIV') { - header = header.nextElementSibling; - } - var content = Array.from(container.children); - // Remove the header from the content array (it isn't nessessary the first element) - content.splice(content.indexOf(header), 1); - - - // Add click event listener to the header - header.addEventListener('click', function () { - console.log("Header clicked: " + header); - // Toggle the collapsible-content class on each content element - content.forEach(function (elem) { - console.log("Toggling collapsible-content class on element: " + elem); - elem.classList.toggle('collapsible-content'); - }); - }); - - // Initialize content as collapsible - content.forEach(function (elem) { - console.log("Adding collapsible-content class to element: " + elem); - elem.classList.add('collapsible-content'); - }); - - // Expand the content initially - header.click(); - }); - */ -} - - document.addEventListener('DOMContentLoaded', () => { - document.getElementById('api').addEventListener('click', () => showModal('yamlDescriptor')); document.getElementById('history').addEventListener('click', () => showModal('sessions')); + document.getElementById('settings').addEventListener('click', () => showModal('settings')); + document.getElementById('usage').addEventListener('click', () => showModal('usage')); document.querySelector('.close').addEventListener('click', closeModal); window.addEventListener('click', (event) => { @@ -111,6 +74,7 @@ document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('form'); const messageInput = document.getElementById('message'); + const usage = document.getElementById('usage'); form.addEventListener('submit', (event) => { event.preventDefault(); @@ -128,6 +92,7 @@ document.addEventListener('DOMContentLoaded', () => { const sessionId = getSessionId(); if (sessionId) { connect(sessionId, onWebSocketText); + usage.href = '/usage/?sessionId=' + sessionId; } else { connect(undefined, onWebSocketText); } @@ -160,6 +125,11 @@ document.addEventListener('DOMContentLoaded', () => { window.open(url, "_blank"); // Open the URL in a new tab }); + const loginLink = document.getElementById('username'); + if (loginLink) { + loginLink.href = '/googleLogin?redirect=' + encodeURIComponent(window.location.pathname); + } + fetch('appInfo') .then(response => { if (!response.ok) { @@ -176,6 +146,21 @@ document.addEventListener('DOMContentLoaded', () => { console.error('There was a problem with the fetch operation:', error); }); - updateMessageDivs(); + fetch('userInfo') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.name && loginLink) { + loginLink.innerHTML = data.name; + loginLink.href = "/userSettings"; + } + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); }); diff --git a/webui/src/main/resources/welcome/chat.css b/webui/src/main/resources/welcome/chat.css index 6ed16ef7..0be5f864 100644 --- a/webui/src/main/resources/welcome/chat.css +++ b/webui/src/main/resources/welcome/chat.css @@ -1,25 +1,3 @@ -#messages { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 40px; - overflow-y: auto; - padding: 10px; - padding-top: 40px; - flex-grow: 1; - flex-shrink: 1; -} - -.message { - background-color: #f0f0f0; - border-radius: 5px; - padding: 10px; - margin-bottom: 10px; - overflow: scroll; - z-index: 100; -} - #form { position: absolute; left: 0; @@ -31,222 +9,40 @@ flex-shrink: 0; } -.reply-form { - width: 100%; -} - -.chat-input { - resize: vertical; -} - -.reply-input { - resize: vertical; -} - -#message { - flex-grow: 1; - margin-right: 5px; +pre { + white-space: pre-wrap; /* Since CSS modules do not automatically apply this */ } -#disconnected-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 999; +#container { display: flex; - justify-content: center; - align-items: center; - color: white; - font-size: 24px; -} - -#disconnected-overlay p { - font-size: 3rem; - line-height: 1.5; - margin-bottom: 20px; - animation-name: bounce; - animation-duration: 0.5s; - animation-iteration-count: infinite; - animation-direction: alternate; - left: 10%; - position: relative; - color: firebrick; -} - -@keyframes bounce { - 0% { - transform: translateY(0); - } - 100% { - transform: translateY(-10px); - } -} - -.spinner-border { - display: block; - width: 40px; - height: 40px; - border: 4px solid rgba(0, 0, 0, 0.1); - border-left-color: #007bff; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + flex-direction: column; + height: 100vh; /* Adjust this value based on your preferred container height */ } -.sr-only { +#applist { + top: 3em; + left: 10px; position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; } - -#toolbar { +#namebar { background-color: #f1f1f1; padding: 5px; position: fixed; - top: 0; width: 100%; - z-index: 1; + top: 0; + right: 0; + text-align: end; + horiz-align: right; + z-index: 2; } -#toolbar a { +#namebar a { color: #000; text-decoration: none; padding: 5px; - display: block; } -#toolbar a:hover { +#namebar a:hover { background-color: #ddd; } - -/* Modal */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0, 0, 0, 0.4); -} - -.modal-content { - background-color: #fefefe; - margin: 15% auto; - padding: 20px; - border: 1px solid #888; - width: 80%; - position: relative; -} - -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - cursor: pointer; -} - -.close:hover, -.close:focus { - color: #000; - text-decoration: none; - cursor: pointer; -} - -pre { - white-space: pre-wrap; /* Since CSS modules do not automatically apply this */ -} - -#container { - display: flex; - flex-direction: column; - height: 100vh; /* Adjust this value based on your preferred container height */ -} - -.play-button { - font-size: 48px; - font-weight: bold; - border: none; - background: transparent; - cursor: pointer; - transition: transform 0.1s; -} - -.play-button:focus { - outline: none; -} - -.play-button:active { - transform: scale(0.9); -} - - -.regen-button { - font-size: 48px; - font-weight: bold; - color: chartreuse; - border: none; - background: transparent; - cursor: pointer; - transition: transform 0.1s; -} - -.regen-button:focus { - outline: none; -} - -.regen-button:active { - transform: scale(0.9); -} - -.message-container { - position: relative; /* Add this line to set the position property of the parent div */ -} - -.cancel-button { - position: absolute; - top: 8px; - right: 8px; - font-size: 16px; - font-weight: bold; - border: none; - background: transparent; - cursor: pointer; - transition: transform 0.1s; -} - -.cancel-button:focus { - outline: none; -} - -.cancel-button:active { - transform: scale(0.9); -} - -.collapsible-content { - overflow: hidden; - max-height: 0; - transition: max-height 0.2s ease-out; -} diff --git a/webui/src/main/resources/welcome/favicon.png b/webui/src/main/resources/welcome/favicon.png deleted file mode 100644 index bec9d99a54d729f50a660141ca5a9f14181ea455..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126746 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ξ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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webui/src/main/resources/welcome/main.js b/webui/src/main/resources/welcome/main.js new file mode 100644 index 00000000..5b94b713 --- /dev/null +++ b/webui/src/main/resources/welcome/main.js @@ -0,0 +1,56 @@ +function showModal(endpoint) { + fetchData(endpoint); + document.getElementById('modal').style.display = 'block'; +} + +function closeModal() { + document.getElementById('modal').style.display = 'none'; +} + +async function fetchData(endpoint) { + try { + // Add session id to the endpoint as a path parameter + const sessionId = getSessionId(); + if (sessionId) { + endpoint = endpoint + "?sessionId=" + sessionId; + } + const response = await fetch(endpoint); + const text = await response.text(); + document.getElementById('modal-content').innerHTML = "

" + text + "
"; + Prism.highlightAll(); + } catch (error) { + console.error('Error fetching data:', error); + } +} + +document.addEventListener('DOMContentLoaded', () => { + + window.addEventListener('click', (event) => { + if (event.target === document.getElementById('modal')) { + closeModal(); + } + }); + + const loginLink = document.getElementById('username'); + if (loginLink) { + loginLink.href = '/googleLogin?redirect=' + encodeURIComponent(window.location.pathname); + } + + fetch('userInfo') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.name && loginLink) { + loginLink.innerHTML = data.name; + loginLink.href = "/userSettings"; + } + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); +}); + diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt index a274bfb4..95ca2f6f 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt @@ -21,7 +21,7 @@ object ActorTestAppServer : AppServerBase(port = 8082) { val type: String? = null, ) - interface JokeParser : Function {} + interface JokeParser : Function override val childWebApps by lazy { listOf( diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/BasicChatTest.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/BasicChatTest.kt deleted file mode 100644 index 2bfb76a2..00000000 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/BasicChatTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "unused") -package com.simiacryptus.skyenet - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.body.SessionDataStorage -import com.simiacryptus.skyenet.body.SkyenetBasicChat -import java.awt.Desktop -import java.net.URI - -object BasicChatTest { - - val api = OpenAIClient(OpenAIClient.keyTxt) - val log = org.slf4j.LoggerFactory.getLogger(BasicChatTest::class.java)!! - var sessionDataStorage: SessionDataStorage? = null - const val port = 8081 - const val baseURL = "http://localhost:$port" - var skyenet = SkyenetBasicChat( - applicationName = "Chat Demo" - ) - - @JvmStatic - fun main(args: Array) { - val httpServer = skyenet.start(port) - sessionDataStorage = skyenet.sessionDataStorage - Desktop.getDesktop().browse(URI(baseURL)) - httpServer.join() - } -} \ No newline at end of file diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/DemoAgent.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/DemoAgent.kt deleted file mode 100644 index 4f5edb04..00000000 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/DemoAgent.kt +++ /dev/null @@ -1,47 +0,0 @@ -@file:Suppress("unused") - -package com.simiacryptus.skyenet - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.util.SessionServerUtil.asJava -import com.simiacryptus.skyenet.body.SkyenetCodingSessionServer -import com.simiacryptus.skyenet.heart.ScalaLocalInterpreter -import com.simiacryptus.util.describe.AbbrevWhitelistYamlDescriber - -import java.awt.Desktop -import java.net.URI - -object DemoAgent { - - class Tools { - fun client() = org.apache.http.impl.client.HttpClients.createDefault() - } - - @JvmStatic - fun main(args: Array) { - val port = 8081 - val baseURL = "http://localhost:$port" - val typeDescriber = AbbrevWhitelistYamlDescriber( - "com.simiacryptus", - ) - val apiKey = OpenAIClient.keyTxt - val server = object : SkyenetCodingSessionServer( - applicationName = "Skyenet Agent Demo", - //oauthConfig = File(File(System.getProperty("user.home")), "client_secret_google_oauth.json").absolutePath, - typeDescriber = typeDescriber, - model = OpenAIClient.Models.GPT35Turbo, - apiKey = apiKey - ) { - override fun hands() = mapOf( - "tools" to Tools() as Object, - ).asJava - -// override fun heart(hands: java.util.Map): Heart = GroovyInterpreter(hands) -// override fun heart(hands: java.util.Map): Heart = KotlinLocalInterpreter(hands) - override fun heart(hands: java.util.Map): Heart = ScalaLocalInterpreter::class.java.getConstructor(java.util.Map::class.java).newInstance(hands) - - }.start(port) - Desktop.getDesktop().browse(URI(baseURL)) - server.join() - } -} \ No newline at end of file diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/FlexmarkChatTest.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/FlexmarkChatTest.kt deleted file mode 100644 index ccd1b999..00000000 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/FlexmarkChatTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "unused") -package com.simiacryptus.skyenet - -import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.body.SessionDataStorage -import com.simiacryptus.skyenet.body.SkyenetMarkupChat -import java.awt.Desktop -import java.net.URI - -object FlexmarkChatTest { - - val api = OpenAIClient(OpenAIClient.keyTxt) - val log = org.slf4j.LoggerFactory.getLogger(FlexmarkChatTest::class.java)!! - var sessionDataStorage: SessionDataStorage? = null - const val port = 8081 - const val baseURL = "http://localhost:$port" - var skyenet = SkyenetMarkupChat( - applicationName = "Chat Demo" - ) - - @JvmStatic - fun main(args: Array) { - val httpServer = skyenet.start(port) - sessionDataStorage = skyenet.sessionDataStorage - Desktop.getDesktop().browse(URI(baseURL)) - httpServer.join() - } -} \ No newline at end of file diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/body/SessionDataStorageTest.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt similarity index 62% rename from webui/src/test/kotlin/com/simiacryptus/skyenet/body/SessionDataStorageTest.kt rename to webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt index bb9e9fc0..27d22157 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/body/SessionDataStorageTest.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt @@ -1,55 +1,40 @@ -package com.simiacryptus.skyenet.body - -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.io.File -import java.nio.file.Files - -class SessionDataStorageTest { - private lateinit var tempDir: File - private lateinit var storage: SessionDataStorage - - @BeforeEach - fun setUp() { - tempDir = Files.createTempDirectory("sessionDataTest").toFile() - storage = SessionDataStorage(dataDir = tempDir) - } - - @AfterEach - fun tearDown() { - tempDir.deleteRecursively() - } - - @Test - fun testUpdateAndLoadOperationStatus() { - val sessionId = "session1" - val operationId = "operation1" - val operationStatus = OperationStatus( - status = OperationStatus.OperationState.Running - ) - - storage.updateOperationStatus(sessionId, operationId, operationStatus) - val operations = storage.loadOperations(sessionId) - - assertEquals(1, operations.size) - assertTrue(operations.containsKey(operationId)) - assertEquals(operationStatus, operations[operationId]) - } - - @Test - fun testUpdateAndLoadMessage() { - val sessionId = "session1" - val messageId = "message1" - val message = "This is a test message." - - storage.updateMessage(sessionId, messageId, message) - val messages = storage.loadMessages(sessionId) - - assertEquals(1, messages.size) - assertTrue(messages.containsKey(messageId)) - assertEquals(message, messages[messageId]) - } -} +package com.simiacryptus.skyenet + +import com.simiacryptus.skyenet.sessions.SessionDataStorage +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.File +import java.nio.file.Files + +class SessionDataStorageTest { + private lateinit var tempDir: File + private lateinit var storage: SessionDataStorage + + @BeforeEach + fun setUp() { + tempDir = Files.createTempDirectory("sessionDataTest").toFile() + storage = SessionDataStorage(dataDir = tempDir) + } + + @AfterEach + fun tearDown() { + tempDir.deleteRecursively() + } + + @Test + fun testUpdateAndLoadMessage() { + val sessionId = "session1" + val messageId = "message1" + val message = "This is a test message." + + storage.updateMessage(sessionId, messageId, message) + val messages = storage.loadMessages(sessionId) + + assertEquals(1, messages.size) + assertTrue(messages.containsKey(messageId)) + assertEquals(message, messages[messageId]) + } +}