From fd0c07bf2d9fc338b1ac746e1c12743c007f1f66 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Tue, 21 Nov 2023 02:10:50 -0500 Subject: [PATCH] 1.0.36 (#40) * 1.0.36 * wip * wip * 1.0.36 --- README.md | 6 +- core/build.gradle.kts | 2 +- .../kotlin/com/simiacryptus/skyenet/Brain.kt | 4 +- .../skyenet/actors/ActorSystem.kt | 12 +- .../simiacryptus/skyenet/actors/BaseActor.kt | 7 +- .../skyenet/actors/CodingActor.kt | 19 +- .../skyenet/actors/ParsedActor.kt | 7 +- .../skyenet/actors/SimpleActor.kt | 5 +- .../skyenet/actors/opt/Expectation.kt | 4 +- .../actors/record/CodingActorInterceptor.kt | 15 +- .../actors/record/ParsedActorInterceptor.kt | 9 +- .../actors/record/SimpleActorInterceptor.kt | 9 +- .../actors/test/ParsedActorTestBase.kt | 2 +- .../skyenet/platform/AuthenticationManager.kt | 18 +- .../skyenet/platform/AuthorizationManager.kt | 6 +- .../skyenet/platform/DataStorage.kt | 194 ++++++++++-------- .../simiacryptus/skyenet/platform/Session.kt | 13 ++ .../skyenet/platform/UsageManager.kt | 71 +++---- .../com/simiacryptus/skyenet/platform/User.kt | 24 +++ .../skyenet/platform/UserSettingsManager.kt | 6 +- .../skyenet/util/FunctionWrapper.kt | 4 +- gradle.properties | 2 +- webui/build.gradle.kts | 2 +- .../simiacryptus/skyenet/ApplicationBase.kt | 114 +++++----- .../simiacryptus/skyenet/chat/ChatServer.kt | 67 +++--- .../simiacryptus/skyenet/chat/ChatSocket.kt | 64 +++--- .../{ChatSession.kt => ChatSocketManager.kt} | 11 +- .../skyenet/chat/CodeChatServer.kt | 11 +- .../skyenet/servlet/AppInfoServlet.kt | 20 ++ .../skyenet/servlet/AuthenticatedWebsite.kt | 13 +- .../skyenet/servlet/FileServlet.kt | 13 +- .../skyenet/servlet/NewSessionServlet.kt | 2 +- .../skyenet/servlet/SessionListServlet.kt | 7 +- .../skyenet/servlet/SessionSettingsServlet.kt | 22 +- .../skyenet/servlet/UsageServlet.kt | 9 +- .../skyenet/servlet/UserSettingsServlet.kt | 4 +- .../skyenet/servlet/WelcomeServlet.kt | 15 +- .../skyenet/servlet/ZipServlet.kt | 10 +- .../skyenet/session/ApplicationInterface.kt | 16 ++ .../session/ApplicationSocketManager.kt | 78 +++++++ .../{SessionDiv.kt => SessionMessage.kt} | 6 +- .../{SessionInterface.kt => SocketManager.kt} | 2 +- .../{SessionBase.kt => SocketManagerBase.kt} | 64 +++--- .../skyenet/test/CodingActorTestApp.kt | 30 +-- .../skyenet/test/ParsedActorTestApp.kt | 26 ++- .../skyenet/test/SimpleActorTestApp.kt | 25 ++- .../skyenet/util/EmbeddingVisualizer.kt | 20 +- .../simiacryptus/skyenet/util/HtmlTools.kt | 31 --- .../skyenet/util/MutableSessionHandler.kt | 10 +- webui/src/main/resources/codeChat/chat.css | 21 +- webui/src/main/resources/codeChat/index.html | 4 + .../src/main/resources/simpleSession/chat.css | 20 +- .../main/resources/simpleSession/index.html | 6 +- webui/src/main/resources/welcome/chat.css | 38 ++++ .../skyenet/ActorTestAppServer.kt | 9 +- 55 files changed, 726 insertions(+), 503 deletions(-) create mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/platform/Session.kt create mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/platform/User.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/chat/{ChatSession.kt => ChatSocketManager.kt} (89%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AppInfoServlet.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationInterface.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationSocketManager.kt rename webui/src/main/kotlin/com/simiacryptus/skyenet/session/{SessionDiv.kt => SessionMessage.kt} (55%) rename webui/src/main/kotlin/com/simiacryptus/skyenet/session/{SessionInterface.kt => SocketManager.kt} (90%) rename webui/src/main/kotlin/com/simiacryptus/skyenet/session/{SessionBase.kt => SocketManagerBase.kt} (75%) delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt diff --git a/README.md b/README.md index 0c9787cd..8442a3a8 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.35 + 1.0.36 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.35' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.36' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.35") +implementation("com.simiacryptus:skyenet:1.0.36") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 3a972a92..653536d1 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -31,7 +31,7 @@ val logback_version = "1.4.11" dependencies { - implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.32") + implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.33") implementation(group = "org.slf4j", name = "slf4j-api", version = "2.0.9") implementation(group = "commons-io", name = "commons-io", version = "2.15.0") diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt index 248a9b65..0e026ef4 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt @@ -119,11 +119,11 @@ open class Brain( companion object { private val log = org.slf4j.LoggerFactory.getLogger(Brain::class.java) fun String.indent(indent: String = " ") = this.replace("\n", "\n$indent") - fun joinYamlList(typeDescriptions: List) = typeDescriptions.joinToString("\n") { + private fun joinYamlList(typeDescriptions: List) = typeDescriptions.joinToString("\n") { "- " + it.indent() } - fun Method.superMethod(): Method? { + private fun Method.superMethod(): Method? { val superMethod = declaringClass.superclasses.flatMap { it.methods.toList() } .find { it.name == name && it.parameters.size == parameters.size } return superMethod?.superMethod() ?: superMethod diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ActorSystem.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ActorSystem.kt index f6bfabca..a579f63c 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ActorSystem.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ActorSystem.kt @@ -2,17 +2,19 @@ package com.simiacryptus.skyenet.actors import com.simiacryptus.skyenet.actors.record.* import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User import com.simiacryptus.skyenet.util.FunctionWrapper import com.simiacryptus.skyenet.util.JsonFunctionRecorder import java.io.File open class ActorSystem>( private val actors: Map>, - private val dataStorage: DataStorage, - val userId: String?, - val sessionId: String + dataStorage: DataStorage, + val userId: User?, + val sessionId: Session ) { - val sessionDir = dataStorage.getSessionDir(userId, sessionId) + private val sessionDir = dataStorage.getSessionDir(userId, sessionId) fun getActor(actor: T): BaseActor<*> { val wrapper = getWrapper(actor.name) return when (val baseActor = actors[actor]) { @@ -26,6 +28,6 @@ open class ActorSystem>( private val wrapperMap = mutableMapOf() private fun getWrapper(name: String) = wrapperMap.computeIfAbsent(name) { - FunctionWrapper(JsonFunctionRecorder(File(sessionDir, "actors/$name"))) + FunctionWrapper(JsonFunctionRecorder(File(sessionDir, ".sys/actors/$name"))) } } 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 27af1824..6eb601cd 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/BaseActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/BaseActor.kt @@ -1,5 +1,6 @@ package com.simiacryptus.skyenet.actors +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.models.OpenAIModel import com.simiacryptus.openai.models.ChatModels import com.simiacryptus.openai.OpenAIClient @@ -12,7 +13,8 @@ abstract class BaseActor( val model: OpenAITextModel = ChatModels.GPT35Turbo, val temperature: Double = 0.3, ) { - open fun response(vararg messages: OpenAIClient.ChatMessage, model: OpenAIModel = this.model, api: OpenAIClient) = api.chat( + abstract fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI): T + open fun response(vararg messages: OpenAIClient.ChatMessage, model: OpenAIModel = this.model, api: OpenAIAPI) = (api as OpenAIClient).chat( OpenAIClient.ChatRequest( messages = ArrayList(messages.toList()), temperature = temperature, @@ -20,8 +22,7 @@ abstract class BaseActor( ), model = this.model ) - 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 answer(vararg questions: String, api: OpenAIAPI): 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 5b63c975..f8b8e1bf 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt @@ -1,6 +1,7 @@ package com.simiacryptus.skyenet.actors import com.fasterxml.jackson.annotation.JsonIgnore +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.OpenAIClientBase.Companion.toContentList import com.simiacryptus.openai.models.ChatModels @@ -66,33 +67,33 @@ open class CodingActor( open val interpreter by lazy { interpreterClass.java.getConstructor(Map::class.java).newInstance(symbols) } - override fun answer(vararg questions: String, api: OpenAIClient): CodeResult = + override fun answer(vararg questions: String, api: OpenAIAPI): CodeResult = if (!autoEvaluate) answer(*chatMessages(*questions), api = api) else answerWithAutoEval(*chatMessages(*questions), api = api).first - override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): CodeResult = - if (!autoEvaluate) CodeResultImpl(*messages, api = api) + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI): CodeResult = + if (!autoEvaluate) CodeResultImpl(*messages, api = (api as OpenAIClient)) else answerWithAutoEval(*messages, api = api).first open fun answerWithPrefix( codePrefix: String, vararg messages: OpenAIClient.ChatMessage, - api: OpenAIClient + api: OpenAIAPI ): CodeResult = - if (!autoEvaluate) CodeResultImpl(*injectCodePrefix(messages, codePrefix), api = api) + if (!autoEvaluate) CodeResultImpl(*injectCodePrefix(messages, codePrefix), api = (api as OpenAIClient)) else answerWithAutoEval(*injectCodePrefix(messages, codePrefix), api = api).first open fun answerWithAutoEval( vararg messages: String, - api: OpenAIClient, + api: OpenAIAPI, codePrefix: String = "" ) = answerWithAutoEval(*injectCodePrefix(chatMessages(*messages), codePrefix), api = api) open fun answerWithAutoEval( vararg messages: OpenAIClient.ChatMessage, - api: OpenAIClient + api: OpenAIAPI ): Pair { - var result = CodeResultImpl(*messages, api = api) + var result = CodeResultImpl(*messages, api = (api as OpenAIClient)) var lastError: Throwable? = null for (i in 0..fixIterations) try { return result to result.run() @@ -159,7 +160,7 @@ open class CodingActor( var codedInstruction = implement( brain(api, model), *messages, codePrefix = codePrefix ) - if (_status != CodeResult.Status.Success) { + if (_status != CodeResult.Status.Success && fallbackModel != model) { codedInstruction = implement( brain(api, fallbackModel), *messages, codePrefix = codePrefix ) 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 153646dc..1ac6097b 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ParsedActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/ParsedActor.kt @@ -1,5 +1,6 @@ package com.simiacryptus.skyenet.actors +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.ChatModels import com.simiacryptus.openai.models.OpenAITextModel @@ -20,10 +21,10 @@ open class ParsedActor( ) { val resultClass: Class by lazy { parserClass.getMethod("apply", String::class.java).returnType as Class } - private inner class ParsedResponseImpl(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient) : ParsedResponse(resultClass) { + private inner class ParsedResponseImpl(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI) : ParsedResponse(resultClass) { private val parser: Function = ChatProxy( clazz = parserClass, - api = api, + api = (api as OpenAIClient), model = ChatModels.GPT35Turbo, temperature = temperature, ).create() @@ -33,7 +34,7 @@ open class ParsedActor( override fun getObj(clazz: Class): T = _obj } - override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): ParsedResponse { + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI): 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 e469e98c..825143a4 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/SimpleActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/SimpleActor.kt @@ -1,5 +1,6 @@ package com.simiacryptus.skyenet.actors +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.ChatModels import com.simiacryptus.openai.models.OpenAITextModel @@ -16,7 +17,7 @@ open class SimpleActor( temperature = temperature, ) { - override fun answer(vararg questions: String, api: OpenAIClient): String = answer(*chatMessages(*questions), api = api) + override fun answer(vararg questions: String, api: OpenAIAPI): String = answer(*chatMessages(*questions), api = api) - override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): String = response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI): String = response(*messages, api = api).choices.first().message?.content ?: throw RuntimeException("No response") } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/opt/Expectation.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/opt/Expectation.kt index d94f3a1f..6e4e6ef6 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/opt/Expectation.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/opt/Expectation.kt @@ -11,7 +11,7 @@ abstract class Expectation { private val log = LoggerFactory.getLogger(Expectation::class.java) } - open class VectorMatch(val example: String, private val metric: DistanceType = DistanceType.Cosine) : Expectation() { + open class VectorMatch(private val example: String, private val metric: DistanceType = DistanceType.Cosine) : Expectation() { override fun matches(api: OpenAIClient, response: String): Boolean { return true } @@ -37,7 +37,7 @@ abstract class Expectation { } open class ContainsMatch( - val pattern: Regex, + private val pattern: Regex, val critical: Boolean = true ) : Expectation() { override fun matches(api: OpenAIClient, response: String): Boolean { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/CodingActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/CodingActorInterceptor.kt index c0b4da68..ea52e26d 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/CodingActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/CodingActorInterceptor.kt @@ -1,5 +1,6 @@ package com.simiacryptus.skyenet.actors.record +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.OpenAIModel import com.simiacryptus.skyenet.actors.CodeResult @@ -20,10 +21,10 @@ class CodingActorInterceptor( temperature = inner.temperature, autoEvaluate = inner.autoEvaluate, ) { - override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): CodeResult { + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI): CodeResult { return functionInterceptor.wrap(messages.toList().toTypedArray()) { messages: Array -> - CodingResultInterceptor(*messages, api = api, inner = inner.answer(*messages, api = api)) + CodingResultInterceptor(*messages, api = (api as OpenAIClient), inner = inner.answer(*messages, api = api)) } } @@ -41,7 +42,7 @@ class CodingActorInterceptor( override fun response( vararg messages: OpenAIClient.ChatMessage, model: OpenAIModel, - api: OpenAIClient + api: OpenAIAPI ) = functionInterceptor.wrap(messages.toList().toTypedArray(), model) { messages: Array, model: OpenAIModel -> @@ -52,14 +53,14 @@ class CodingActorInterceptor( inner.chatMessages(*it) } - override fun answer(vararg questions: String, api: OpenAIClient) = functionInterceptor.wrap(questions) { + override fun answer(vararg questions: String, api: OpenAIAPI) = functionInterceptor.wrap(questions) { inner.answer(*it, api = api) } override fun answerWithPrefix( codePrefix: String, vararg messages: OpenAIClient.ChatMessage, - api: OpenAIClient + api: OpenAIAPI ) = functionInterceptor.wrap(messages.toList().toTypedArray(), codePrefix) { messages: Array, codePrefix: String -> @@ -68,7 +69,7 @@ class CodingActorInterceptor( override fun answerWithAutoEval( vararg messages: String, - api: OpenAIClient, + api: OpenAIAPI, codePrefix: String ) = functionInterceptor.wrap(messages.toList().toTypedArray(), codePrefix) { messages: Array, @@ -78,7 +79,7 @@ class CodingActorInterceptor( override fun answerWithAutoEval( vararg messages: OpenAIClient.ChatMessage, - api: OpenAIClient + api: OpenAIAPI ) = functionInterceptor.wrap(messages.toList().toTypedArray()) { inner.answerWithAutoEval(*messages, api = api) } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/ParsedActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/ParsedActorInterceptor.kt index 693b8ebc..d3619907 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/ParsedActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/ParsedActorInterceptor.kt @@ -1,5 +1,6 @@ package com.simiacryptus.skyenet.actors.record +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.OpenAIModel import com.simiacryptus.skyenet.actors.ParsedActor @@ -16,13 +17,13 @@ class ParsedActorInterceptor( model = inner.model, temperature = inner.temperature, ) { - private inner class ParsedResponseInterceptor(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient, private val inner: ParsedResponse) : + private inner class ParsedResponseInterceptor(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI, private val inner: ParsedResponse) : ParsedResponse(this@ParsedActorInterceptor.inner.resultClass) { override fun getText() = functionInterceptor.wrap { inner.getText() } override fun getObj(clazz: Class) = functionInterceptor.intercept(clazz) { inner.getObj(clazz) } // <-- Cannot use 'T' as reified type parameter. Use a class instead. } - override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient): ParsedResponse { + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI): ParsedResponse { return functionInterceptor.wrap(messages.toList().toTypedArray()) { messages: Array -> ParsedResponseInterceptor(*messages, api = api, inner = inner.answer(*messages, api = api)) @@ -32,14 +33,14 @@ class ParsedActorInterceptor( override fun response( vararg messages: OpenAIClient.ChatMessage, model: OpenAIModel, - api: OpenAIClient + api: OpenAIAPI ) = functionInterceptor.wrap(messages.toList().toTypedArray(), model) { messages: Array, model: OpenAIModel -> inner.response(*messages, model = model, api = api) } - override fun answer(vararg questions: String, api: OpenAIClient) = functionInterceptor.wrap(questions) { + override fun answer(vararg questions: String, api: OpenAIAPI) = functionInterceptor.wrap(questions) { inner.answer(*it, api = api) } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/SimpleActorInterceptor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/SimpleActorInterceptor.kt index c2ca3e39..4e90f18e 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/SimpleActorInterceptor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/record/SimpleActorInterceptor.kt @@ -1,5 +1,6 @@ package com.simiacryptus.skyenet.actors.record +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.OpenAIModel import com.simiacryptus.skyenet.actors.SimpleActor @@ -7,7 +8,7 @@ import com.simiacryptus.skyenet.util.FunctionWrapper class SimpleActorInterceptor( val inner: SimpleActor, - val functionInterceptor: FunctionWrapper, + private val functionInterceptor: FunctionWrapper, ) : SimpleActor( prompt = inner.prompt, name = inner.name, @@ -15,7 +16,7 @@ class SimpleActorInterceptor( temperature = inner.temperature, ) { - override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIClient) = + override fun answer(vararg messages: OpenAIClient.ChatMessage, api: OpenAIAPI) = functionInterceptor.wrap(messages.toList().toTypedArray()) { messages: Array -> inner.answer(*messages, api = api) @@ -24,7 +25,7 @@ class SimpleActorInterceptor( override fun response( vararg messages: OpenAIClient.ChatMessage, model: OpenAIModel, - api: OpenAIClient + api: OpenAIAPI ) = functionInterceptor.wrap(messages.toList().toTypedArray(), model) { messages: Array, model: OpenAIModel -> @@ -35,7 +36,7 @@ class SimpleActorInterceptor( inner.chatMessages(*it) } - override fun answer(vararg questions: String, api: OpenAIClient) = functionInterceptor.wrap(questions) { + override fun answer(vararg questions: String, api: OpenAIAPI) = functionInterceptor.wrap(questions) { inner.answer(*it, api = api) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/test/ParsedActorTestBase.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/test/ParsedActorTestBase.kt index 739d3bfd..f271fd7d 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/test/ParsedActorTestBase.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/test/ParsedActorTestBase.kt @@ -6,7 +6,7 @@ import com.simiacryptus.skyenet.actors.ParsedResponse import java.util.function.Function abstract class ParsedActorTestBase( - val parserClass: Class>, + private val parserClass: Class>, ) : ActorTestBase>() { override fun actorFactory(prompt: String) = ParsedActor( diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthenticationManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthenticationManager.kt index 71c729c6..7c3df51f 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthenticationManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthenticationManager.kt @@ -4,25 +4,17 @@ import java.util.HashMap open class AuthenticationManager { - data class UserInfo( - val id: String, - val email: String, - val name: String, - val picture: String - ) - - private val users = HashMap() + private val users = HashMap() open fun getUser(sessionId: String?) = if (null == sessionId) null else users[sessionId] - open fun containsKey(value: String): Boolean = users.containsKey(value) + open fun containsUser(value: String): Boolean = users.containsKey(value) - open fun setUser(sessionId: String, userInfo: UserInfo) { - users[sessionId] = userInfo + open fun putUser(sessionId: String, user: User) { + users[sessionId] = user } companion object { - const val COOKIE_NAME = "sessionId" - private val log = org.slf4j.LoggerFactory.getLogger(AuthenticationManager::class.java) + const val AUTH_COOKIE = "sessionId" } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthorizationManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthorizationManager.kt index 65b87ac8..a89723e8 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthorizationManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/AuthorizationManager.kt @@ -15,16 +15,16 @@ open class AuthorizationManager { open fun isAuthorized( applicationClass: Class<*>?, - user: String?, + user: User?, operationType: OperationType, ) = try { - if (isUserAuthorized("/permissions/${operationType.name.lowercase(Locale.getDefault())}.txt", user)) { + if (isUserAuthorized("/permissions/${operationType.name.lowercase(Locale.getDefault())}.txt", user?.email)) { log.debug("User {} authorized for {} globally", user, operationType) true } else if (null != applicationClass) { val packagePath = applicationClass.`package`.name.replace('.', '/') val opName = operationType.name.lowercase(Locale.getDefault()) - if (isUserAuthorized("/$packagePath/$opName.txt", user)) { + if (isUserAuthorized("/$packagePath/$opName.txt", user?.email)) { log.debug("User {} authorized for {} on {}", user, operationType, applicationClass) true } else { diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/DataStorage.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/DataStorage.kt index 8627cfa3..e966adf5 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/DataStorage.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/DataStorage.kt @@ -6,63 +6,90 @@ import java.util.* open class DataStorage( - val dataDir: File + private val dataDir: File ) { - open fun updateMessage(userId: String?, sessionId: String, messageId: String, value: String) { - validateSessionId(sessionId) - val file = File(File(this.getSessionDir(userId, sessionId), MESSAGE_DIR), "$messageId.json") - log.debug("Updating message for $sessionId / $messageId: ${file.absolutePath}") - file.parentFile.mkdirs() - JsonUtil.objectMapper().writeValue(file, value) + open fun getJson( + user: User?, + session: Session, + filename: String, + clazz: Class + ): T? { + validateSessionId(session) + val settingsFile = File(this.getSessionDir(user, session), filename) + return if (!settingsFile.exists()) null else { + JsonUtil.objectMapper().readValue(settingsFile, clazz) as T + } } - open fun getMessages(userId: String?, sessionId: String): LinkedHashMap { - validateSessionId(sessionId) - val messageDir = File(this.getSessionDir(userId, sessionId), MESSAGE_DIR) + open fun getMessages( + user: User?, + session: Session + ): LinkedHashMap { + validateSessionId(session) + val messageDir = File(this.getSessionDir(user, session), MESSAGE_DIR) val messages = LinkedHashMap() - log.debug("Loading messages for $sessionId: ${messageDir.absolutePath}") + log.debug("Loading messages for {}: {}", session, messageDir.absolutePath) messageDir.listFiles()?.sortedBy { it.lastModified() }?.forEach { file -> val message = JsonUtil.objectMapper().readValue(file, String::class.java) messages[file.nameWithoutExtension] = message } - log.debug("Loaded ${messages.size} messages for $sessionId") + log.debug("Loaded {} messages for {}", messages.size, session) return messages } - open fun listSessions(userId: String?): List { - val globalSessions = listSessions(dataDir) - val userSessions = if(userId==null) listOf() else listSessions(userRoot(userId)) - return globalSessions.map { "G-$it" } + userSessions.map { "U-$it" } - } + open fun getSessionDir( + user: User?, + session: Session + ): File { + validateSessionId(session) + val parts = session.sessionId.split("-") + return when (parts.size) { + 3 -> { + val root = when { + parts[0] == "G" -> dataDir + parts[0] == "U" -> userRoot(user) + else -> throw IllegalArgumentException("Invalid session ID: $session") + } + val dateDir = File(root, parts[1]) + log.debug("Date Dir for {}: {}", session, dateDir.absolutePath) + val sessionDir = File(dateDir, parts[2]) + log.debug("Instance Dir for {}: {}", session, sessionDir.absolutePath) + sessionDir + } - private fun listSessions(dir: File): List { - val files = dir.listFiles()?.flatMap { it.listFiles()?.toList() ?: listOf() }?.filter { sessionDir -> - val operationDir = File(sessionDir, MESSAGE_DIR) - if (!operationDir.exists()) false else { - val listFiles = operationDir.listFiles()?.filter { it.isFile && !it.name.startsWith("aaa") } - (listFiles?.size ?: 0) > 0 + 2 -> { + val dateDir = File(dataDir, parts[0]) + log.debug("Date Dir for {}: {}", session, dateDir.absolutePath) + val sessionDir = File(dateDir, parts[1]) + log.debug("Instance Dir for {}: {}", session, sessionDir.absolutePath) + sessionDir + } + + else -> { + throw IllegalArgumentException("Invalid session ID: $session") } } - log.debug("Sessions: {}", files?.map { it.parentFile.name + "-" + it.name }) - return files?.map { it.parentFile.name + "-" + it.name } ?: listOf() } - open fun getSessionName(userId: String?, sessionId: String): String { - validateSessionId(sessionId) - val userMessage = File(this.getSessionDir(userId, sessionId), MESSAGE_DIR).listFiles() + open fun getSessionName( + user: User?, + session: Session + ): String { + validateSessionId(session) + val userMessage = File(this.getSessionDir(user, session), MESSAGE_DIR).listFiles() ?.filter { file -> file.isFile } ?.sortedBy { file -> file.lastModified() } ?.map { messageFile -> val fileText = messageFile.readText() val split = fileText.split("

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

") if (stringList.isEmpty()) { - log.debug("Session $sessionId: No messages") + log.debug("Session {}: No messages", session) "" } else { stringList.first() @@ -70,94 +97,93 @@ open class DataStorage( } }?.firstOrNull { it.isNotEmpty() } return if (null != userMessage) { - log.debug("Session $sessionId: $userMessage") + log.debug("Session {}: {}", session, userMessage) userMessage } else { - log.debug("Session $sessionId: No messages") - sessionId + log.debug("Session {}: No messages", session) + session.sessionId } } - open fun getJson(userId: String?, sessionId: String, clazz: Class, filename: String): T? { - validateSessionId(sessionId) - val settingsFile = File(this.getSessionDir(userId, sessionId), filename) - return if (!settingsFile.exists()) null else { - JsonUtil.objectMapper().readValue(settingsFile, clazz) as T - } + open fun listSessions( + user: User? + ): List { + val globalSessions = listSessions(dataDir) + val userSessions = if (user == null) listOf() else listSessions(userRoot(user)) + return globalSessions.map { Session("G-$it") } + userSessions.map { Session("U-$it") } } - open fun setJson(userId: String?, sessionId: String, settings: T, filename: String): T { - validateSessionId(sessionId) - val settingsFile = File(this.getSessionDir(userId, sessionId), filename) + open fun setJson( + user: User?, + session: Session, + filename: String, + settings: T + ): T { + validateSessionId(session) + val settingsFile = File(this.getSessionDir(user, session), filename) settingsFile.parentFile.mkdirs() JsonUtil.objectMapper().writeValue(settingsFile, settings) return settings } - open fun getSessionDir(userId: String?, sessionId: String): File { - validateSessionId(sessionId) - val parts = sessionId.split("-") - return when (parts.size) { - 3 -> { - val root = when { - parts[0] == "G" -> dataDir - parts[0] == "U" -> userRoot(userId) - else -> throw IllegalArgumentException("Invalid session ID: $sessionId") - } - val dateDir = File(root, parts[1]) - log.debug("Date Dir for $sessionId: ${dateDir.absolutePath}") - val sessionDir = File(dateDir, parts[2]) - log.debug("Instance Dir for $sessionId: ${sessionDir.absolutePath}") - sessionDir - } - - 2 -> { - val dateDir = File(dataDir, parts[0]) - log.debug("Date Dir for $sessionId: ${dateDir.absolutePath}") - val sessionDir = File(dateDir, parts[1]) - log.debug("Instance Dir for $sessionId: ${sessionDir.absolutePath}") - sessionDir - } + open fun updateMessage( + user: User?, + session: Session, + messageId: String, + value: String + ) { + validateSessionId(session) + val file = File(File(this.getSessionDir(user, session), MESSAGE_DIR), "$messageId.json") + log.debug("Updating message for {} / {}: {}", session, messageId, file.absolutePath) + file.parentFile.mkdirs() + JsonUtil.objectMapper().writeValue(file, value) + } - else -> { - throw IllegalArgumentException("Invalid session ID: $sessionId") + private fun listSessions(dir: File): List { + val files = dir.listFiles()?.flatMap { it.listFiles()?.toList() ?: listOf() }?.filter { sessionDir -> + val operationDir = File(sessionDir, MESSAGE_DIR) + 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() } - private fun userRoot(userId: String?) = File( + private fun userRoot(user: User?) = File( File(dataDir, "users"), - userId ?: throw IllegalArgumentException("User ID required for private session") + user?.email ?: throw IllegalArgumentException("User required for private session") ) - open fun validateSessionId(sessionId: String) { - if (!sessionId.matches("""([GU]-)?\d{8}-\w{8}""".toRegex())) { - throw IllegalArgumentException("Invalid session ID: $sessionId") - } - } - companion object { private val log = org.slf4j.LoggerFactory.getLogger(DataStorage::class.java) - fun newGlobalID(): String { + fun validateSessionId( + session: Session + ) { + if (!session.sessionId.matches("""([GU]-)?\d{8}-\w{8}""".toRegex())) { + throw IllegalArgumentException("Invalid session ID: $session") + } + } + + fun newGlobalID(): Session { val uuid = UUID.randomUUID().toString().split("-").first() val yyyyMMdd = java.time.LocalDate.now().toString().replace("-", "") log.debug("New ID: $yyyyMMdd-$uuid") - return "G-$yyyyMMdd-$uuid" + return Session("G-$yyyyMMdd-$uuid") } - fun newUserID(): String { + fun newUserID(): Session { val uuid = UUID.randomUUID().toString().split("-").first() val yyyyMMdd = java.time.LocalDate.now().toString().replace("-", "") log.debug("New ID: $yyyyMMdd-$uuid") - return "U-$yyyyMMdd-$uuid" + return Session("U-$yyyyMMdd-$uuid") } - private const val MESSAGE_DIR = "messages" - fun String.stripPrefix(prefix: String) = if (!this.startsWith(prefix)) this else { - this.substring(prefix.length) - } + private val MESSAGE_DIR = ".sys" + File.separator + "messages" } } + diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/Session.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/Session.kt new file mode 100644 index 00000000..871f7645 --- /dev/null +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/Session.kt @@ -0,0 +1,13 @@ +package com.simiacryptus.skyenet.platform + +import com.simiacryptus.skyenet.platform.DataStorage.Companion.validateSessionId + +data class Session( + internal val sessionId: String +) { + init { + validateSessionId(this) + } + + override fun toString() = sessionId +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UsageManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UsageManager.kt index db28493f..99c663c5 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UsageManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UsageManager.kt @@ -5,6 +5,7 @@ import com.simiacryptus.openai.models.* import com.simiacryptus.util.JsonUtil import java.io.File import java.io.FileWriter +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -14,9 +15,9 @@ open class UsageManager { private val scheduler = Executors.newSingleThreadScheduledExecutor() private val txLogFile = File(".skyenet/usage/log.csv") @Volatile private var txLogFileWriter: FileWriter? - private val usagePerSession = HashMap() - private val sessionsByUser = HashMap>() - private val usersBySession = HashMap>() + private val usagePerSession = ConcurrentHashMap() + private val sessionsByUser = ConcurrentHashMap>() + private val usersBySession = ConcurrentHashMap>() init { txLogFile.parentFile.mkdirs() @@ -40,15 +41,15 @@ open class UsageManager { ?: throw RuntimeException("Unknown model $model") when (direction) { "input" -> incrementUsage( - sessionId, - user, + Session(sessionId), + User(email=user), modelEnum, OpenAIClient.Usage(prompt_tokens = tokens.toInt()) ) "output" -> incrementUsage( - sessionId, - user, + Session(sessionId), + User(email=user), modelEnum, OpenAIClient.Usage(completion_tokens = tokens.toInt()) ) @@ -63,17 +64,17 @@ open class UsageManager { } @Suppress("MemberVisibilityCanBePrivate") - open fun writeCompactLog(file: File) { - val writer = FileWriter(file) - usagePerSession.forEach { (sessionId, usage) -> - val user = usersBySession[sessionId]?.firstOrNull() - usage.tokensPerModel.forEach { (model, counter) -> - writer.write("$sessionId,$user,${model.model.modelName},${counter.inputTokens.get()},input\n") - writer.write("$sessionId,$user,${model.model.modelName},${counter.outputTokens.get()},output\n") + protected open fun writeCompactLog(file: File) { + FileWriter(file).use { writer -> + usagePerSession.forEach { (sessionId, usage) -> + val user = usersBySession[sessionId]?.firstOrNull() + usage.tokensPerModel.forEach { (model, counter) -> + writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.inputTokens.get()},input\n") + writer.write("$sessionId,${user?.email},${model.model.modelName},${counter.outputTokens.get()},output\n") + } } + writer.flush() } - writer.flush() - writer.close() } private fun saveCounters() { @@ -107,26 +108,20 @@ open class UsageManager { File(".skyenet/usage/counters.json").writeText(JsonUtil.toJson(usagePerSession)) } - open fun incrementUsage(sessionId: String, user: String?, model: OpenAIModel, tokens: OpenAIClient.Usage) { - val usage = usagePerSession.getOrPut(sessionId) { - UsageCounters() - } - val tokensPerModel = usage.tokensPerModel.getOrPut(UsageKey(sessionId, user, model)) { - UsageValues() - } - tokensPerModel.addAndGet(tokens) + open fun incrementUsage(session: Session, user: User?, model: OpenAIModel, tokens: OpenAIClient.Usage) { + @Suppress("NAME_SHADOWING") val user = if (null == user) null else User(email = user.email) // Hack + usagePerSession.computeIfAbsent(session) { UsageCounters() } + .tokensPerModel.computeIfAbsent(UsageKey(session, user, model)) { UsageValues() } + .addAndGet(tokens) if (user != null) { - val sessions = sessionsByUser.getOrPut(user) { - HashSet() - } - sessions.add(sessionId) + sessionsByUser.computeIfAbsent(user) { HashSet() }.add(session) } try { val txLogFileWriter = txLogFileWriter - if(null != txLogFileWriter) { + if (null != txLogFileWriter) { synchronized(txLogFile) { - txLogFileWriter.write("$sessionId,$user,${model.modelName},${tokens.prompt_tokens},input\n") - txLogFileWriter.write("$sessionId,$user,${model.modelName},${tokens.completion_tokens},output\n") + txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.prompt_tokens},input\n") + txLogFileWriter.write("$session,${user?.email},${model.modelName},${tokens.completion_tokens},output\n") txLogFileWriter.flush() } } @@ -135,8 +130,9 @@ open class UsageManager { } } - open fun getUserUsageSummary(user: String): Map = - sessionsByUser[user]?.flatMap { sessionId -> + open fun getUserUsageSummary(user: User): Map { + @Suppress("NAME_SHADOWING") val user = if(null == user) null else User(email= user.email) // Hack + return sessionsByUser[user]?.flatMap { sessionId -> val usage = usagePerSession[sessionId] usage?.tokensPerModel?.entries?.map { (model, counter) -> model.model to counter.toUsage() @@ -149,9 +145,10 @@ open class UsageManager { ) } } ?: emptyMap() + } - open fun getSessionUsageSummary(sessionId: String): Map = - usagePerSession[sessionId]?.tokensPerModel?.entries?.map { (model, counter) -> + open fun getSessionUsageSummary(session: Session): Map = + usagePerSession[session]?.tokensPerModel?.entries?.map { (model, counter) -> model.model to counter.toUsage() }?.groupBy { it.first }?.mapValues { it.value.map { it.second }.reduce { a, b -> OpenAIClient.Usage( @@ -161,8 +158,8 @@ open class UsageManager { } } ?: emptyMap() data class UsageKey( - val sessionId: String, - val user: String?, + val session: Session, + val user: User?, val model: OpenAIModel, ) diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/User.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/User.kt new file mode 100644 index 00000000..1b2d4d8a --- /dev/null +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/User.kt @@ -0,0 +1,24 @@ +package com.simiacryptus.skyenet.platform + +data class User( + internal val email: String, + internal val name: String? = null, + internal val id: String? = null, + internal val picture: String? = null, +) { + override fun toString() = email + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as User + + return email == other.email + } + + override fun hashCode(): Int { + return email.hashCode() + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UserSettingsManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UserSettingsManager.kt index 6cec9b29..e15a8054 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UserSettingsManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/platform/UserSettingsManager.kt @@ -8,10 +8,10 @@ open class UserSettingsManager { val apiKey: String = "", ) - private val userSettings = HashMap() + private val userSettings = HashMap() private val userConfigDirectory = File(".skyenet/users") - open fun getUserSettings(user: String): UserSettings { + open fun getUserSettings(user: User): UserSettings { return userSettings.getOrPut(user) { val file = File(userConfigDirectory, "$user.json") if (file.exists()) { @@ -24,7 +24,7 @@ open class UserSettingsManager { } } - open fun updateUserSettings(user: String, settings: UserSettings) { + open fun updateUserSettings(user: User, settings: UserSettings) { userSettings[user] = settings val file = File(userConfigDirectory, "$user.json") file.parentFile.mkdirs() diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/util/FunctionWrapper.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/util/FunctionWrapper.kt index 4092199a..1bbfe387 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/util/FunctionWrapper.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/util/FunctionWrapper.kt @@ -80,7 +80,9 @@ class JsonFunctionRecorder(baseDir: File) : FunctionInterceptor, Closeable { ) // Get the caller method name from the stack trace (first caller not in internalClassList) val caller = Thread.currentThread().stackTrace - .firstOrNull { !internalClassList.contains(Class.forName(it.className)) } + .filter { !internalClassList.contains(Class.forName(it.className)) } + .filter { it.methodName != "intercept" } + .firstOrNull() val methodName = caller?.methodName ?: "unknown" val file = File(baseDirectory, "$id-$yyyyMMddHHmmss-$methodName") if(file.exists()) { diff --git a/gradle.properties b/gradle.properties index a0264da7..536076d2 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.35 +libraryVersion = 1.0.36 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/webui/build.gradle.kts b/webui/build.gradle.kts index 7cae2c95..55657fce 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -32,7 +32,7 @@ val jetty_version = "11.0.18" val jackson_version = "2.15.3" dependencies { - implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.32") + implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.33") implementation(project(":core")) testImplementation(project(":groovy")) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt index a811b360..81077f9f 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt @@ -1,17 +1,21 @@ package com.simiacryptus.skyenet +import com.simiacryptus.openai.OpenAIAPI +import com.simiacryptus.skyenet.servlet.AppInfoServlet import com.simiacryptus.skyenet.chat.ChatServer -import com.simiacryptus.skyenet.chat.ChatSocket import com.simiacryptus.skyenet.platform.ApplicationServices.authenticationManager import com.simiacryptus.skyenet.platform.ApplicationServices.authorizationManager import com.simiacryptus.skyenet.platform.ApplicationServices.dataStorageFactory import com.simiacryptus.skyenet.servlet.* -import com.simiacryptus.skyenet.platform.AuthenticationManager.Companion.COOKIE_NAME -import com.simiacryptus.skyenet.session.SessionBase -import com.simiacryptus.skyenet.session.SessionDiv -import com.simiacryptus.skyenet.session.SessionInterface +import com.simiacryptus.skyenet.platform.AuthenticationManager.Companion.AUTH_COOKIE +import com.simiacryptus.skyenet.session.SessionMessage +import com.simiacryptus.skyenet.session.SocketManager import com.simiacryptus.skyenet.platform.AuthorizationManager -import com.simiacryptus.skyenet.util.HtmlTools +import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User +import com.simiacryptus.skyenet.session.ApplicationInterface +import com.simiacryptus.skyenet.session.ApplicationSocketManager import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.eclipse.jetty.servlet.FilterHolder @@ -26,77 +30,64 @@ abstract class ApplicationBase( val temperature: Double = 0.1, ) : ChatServer(resourceBase) { + final override val dataStorage: DataStorage = dataStorageFactory(File(File(".skyenet"), applicationName)) + protected open val appInfo = ServletHolder("appInfo", AppInfoServlet(applicationName)) + protected open val userInfo = ServletHolder("userInfo", UserInfoServlet()) + protected open val usageServlet = ServletHolder("usage", UsageServlet()) + protected open val fileZip = ServletHolder("fileZip", ZipServlet(dataStorage)) + protected open val fileIndex = ServletHolder("fileIndex", FileServlet(dataStorage)) + protected open val sessionSettingsServlet = ServletHolder("settings", SessionSettingsServlet(this)) - inner class ApplicationSession( - sessionId: String, - userId: String?, - ) : SessionBase( - sessionId = sessionId, - dataStorage = dataStorage, - userId = userId, - applicationClass = this@ApplicationBase.javaClass, - ) { - private val threads = mutableMapOf() - - val linkTriggers = mutableMapOf>() - - override fun onRun(userMessage: String, socket: ChatSocket) { - val operationID = randomID() - val sessionDiv = newSessionDiv(operationID, spinner, true) - threads[operationID] = Thread.currentThread() - processMessage(sessionId, userMessage, this, sessionDiv, socket) - } - - override fun onCmd(id: String, code: String, socket: ChatSocket) { - if (code == "cancel") { - threads[id]?.interrupt() - } else if (code == "link") { - val consumer = linkTriggers[id] - consumer ?: throw IllegalArgumentException("No link handler found") - consumer.accept(Unit) - } else { - throw IllegalArgumentException("Unknown command: $code") - } + override fun newSession(user: User?, session: Session): SocketManager { + return object : ApplicationSocketManager( + session = session, + userId = user, + dataStorage = dataStorage, + applicationClass = this@ApplicationBase::class.java, + ) { + override fun newSession( + session: Session, + user: User?, + userMessage: String, + socketManager: ApplicationSocketManager, + sessionMessage: SessionMessage, + api: OpenAIAPI + ) = this@ApplicationBase.newSession( + session = session, + user = user, + userMessage = userMessage, + socketManager = socketManager.applicationInterface, + sessionMessage = sessionMessage, + api = api + ) } - - fun htmlTools(divID: String) = HtmlTools(this, divID) } - override fun newSession(userId: String?, sessionId: String): SessionInterface { - return ApplicationSession(sessionId, userId) - } - - abstract fun processMessage( - sessionId: String, + abstract fun newSession( + session: Session, + user: User?, userMessage: String, - session: ApplicationSession, - sessionDiv: SessionDiv, - socket: ChatSocket + socketManager: ApplicationInterface, + sessionMessage: SessionMessage, + api: OpenAIAPI ) open val settingsClass: Class<*> get() = Map::class.java - open fun initSettings(sessionId: String): T? = null + open fun initSettings(session: Session): T? = null - fun getSettings(sessionId: String, userId: String?): T? { + fun getSettings(session: Session, userId: User?): T? { @Suppress("UNCHECKED_CAST") - var settings: T? = dataStorage.getJson(userId, sessionId, settingsClass as Class, "settings.json") + var settings: T? = dataStorage.getJson(userId, session, "settings.json", settingsClass as Class) if (null == settings) { - settings = initSettings(sessionId) + settings = initSettings(session) if (null != settings) { - dataStorage.setJson(userId, sessionId, settings, "settings.json") + dataStorage.setJson(userId, session, "settings.json", settings) } } return settings } - final override val dataStorage = dataStorageFactory(File(File(".skyenet"), applicationName)) - protected open val appInfo = ServletHolder("appInfo", AppInfoServlet()) - protected open val userInfo = ServletHolder("userInfo", UserInfoServlet()) - protected open val usageServlet = ServletHolder("usage", UsageServlet()) - protected open val fileZip = ServletHolder("fileZip", ZipServlet(dataStorage)) - protected open val fileIndex = ServletHolder("fileIndex", FileServlet(dataStorage)) - protected open val sessionSettingsServlet = ServletHolder("settings", SessionSettingsServlet(this)) protected open fun sessionsServlet(path: String) = ServletHolder("sessionList", SessionListServlet(this.dataStorage, path)) override fun configure(webAppContext: WebAppContext, path: String, baseUrl: String) { @@ -107,7 +98,7 @@ abstract class ApplicationBase( val user = authenticationManager.getUser((request as HttpServletRequest).getCookie()) val canRead = authorizationManager.isAuthorized( applicationClass = this@ApplicationBase.javaClass, - user = user?.email, + user = user, operationType = AuthorizationManager.OperationType.Read ) if (canRead) { @@ -128,7 +119,6 @@ abstract class ApplicationBase( webAppContext.addServlet(sessionSettingsServlet, "/settings") } - companion object { private val log = LoggerFactory.getLogger(ApplicationBase::class.java) val spinner = @@ -147,7 +137,7 @@ abstract class ApplicationBase( filename.endsWith(".css") -> "text/css" else -> "text/plain" } - fun HttpServletRequest.getCookie(name: String = COOKIE_NAME) = cookies?.find { it.name == name }?.value + fun HttpServletRequest.getCookie(name: String = AUTH_COOKIE) = cookies?.find { it.name == name }?.value } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatServer.kt index 2e514d72..b3621438 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatServer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatServer.kt @@ -1,14 +1,10 @@ package com.simiacryptus.skyenet.chat -import com.simiacryptus.skyenet.platform.ApplicationServices -import com.simiacryptus.skyenet.platform.AuthenticationManager -import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.* +import com.simiacryptus.skyenet.platform.ApplicationServices.authenticationManager +import com.simiacryptus.skyenet.platform.AuthenticationManager.Companion.AUTH_COOKIE import com.simiacryptus.skyenet.servlet.NewSessionServlet -import com.simiacryptus.skyenet.session.SessionInterface -import com.simiacryptus.util.JsonUtil -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse +import com.simiacryptus.skyenet.session.SocketManager import org.eclipse.jetty.servlet.DefaultServlet import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.util.resource.Resource @@ -19,60 +15,48 @@ import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory abstract class ChatServer(val resourceBase: String) { - fun JettyServerUpgradeRequest.getCookie(name: String) = cookies?.find { it.name == name }?.value - abstract val applicationName: String open val dataStorage: DataStorage? = null - val stateCache: MutableMap = mutableMapOf() inner class WebSocketHandler : JettyWebSocketServlet() { + private val stateCache: MutableMap = mutableMapOf() override fun configure(factory: JettyWebSocketServletFactory) { factory.setCreator { req, resp -> try { - val sessionId = req.parameterMap["sessionId"]?.firstOrNull() - val authId = req.getCookie(AuthenticationManager.COOKIE_NAME) - return@setCreator if (null == sessionId) { + val authId = req.getCookie(AUTH_COOKIE) + return@setCreator if (!req.parameterMap.containsKey("sessionId")) { null } else { - val sessionState: SessionInterface - if (stateCache.containsKey(sessionId)) { - sessionState = stateCache[sessionId]!! - } else { - sessionState = newSession( - ApplicationServices.authenticationManager.getUser( - req.getCookie(AuthenticationManager.COOKIE_NAME) - )?.id, sessionId) - stateCache[sessionId] = sessionState - } - ChatSocket(sessionId, sessionState, dataStorage, ApplicationServices.authenticationManager.getUser(authId)) + val session = Session(req.parameterMap["sessionId"]?.first()!!) + val sessionState: SocketManager = getSession(session, req) + val user = authenticationManager.getUser(authId) + ChatSocket(session, sessionState, dataStorage, user) } } catch (e: Exception) { log.warn("Error configuring websocket", e) } } } - } - 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 - ) - ) - ) + private fun getSession( + session: Session, + req: JettyServerUpgradeRequest + ) = if (stateCache.containsKey(session)) { + stateCache[session]!! + } else { + val user = authenticationManager.getUser(req.getCookie(AUTH_COOKIE)) + val sessionState = newSession(user, session) + stateCache[session] = sessionState + sessionState } } - abstract fun newSession(userId: String?, sessionId: String): SessionInterface + abstract fun newSession(user: User?, session: Session): SocketManager 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() } + private val newSessionServlet by lazy { NewSessionServlet() } + private val webSocketHandler by lazy { WebSocketHandler() } + private val defaultServlet by lazy { DefaultServlet() } open fun configure(webAppContext: WebAppContext, path: String = "/", baseUrl: String) { webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/default", defaultServlet), "/") @@ -82,6 +66,7 @@ abstract class ChatServer(val resourceBase: String) { companion object { private val log = org.slf4j.LoggerFactory.getLogger(ChatServer::class.java) + fun JettyServerUpgradeRequest.getCookie(name: String) = cookies?.find { it.name == name }?.value } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocket.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocket.kt index f91db2b5..6b788f45 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocket.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocket.kt @@ -2,67 +2,67 @@ package com.simiacryptus.skyenet.chat import com.simiacryptus.openai.models.OpenAIModel import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.platform.ApplicationServices +import com.simiacryptus.skyenet.platform.* import com.simiacryptus.skyenet.platform.ApplicationServices.authorizationManager -import com.simiacryptus.skyenet.platform.AuthenticationManager -import com.simiacryptus.skyenet.platform.DataStorage -import com.simiacryptus.skyenet.session.SessionInterface +import com.simiacryptus.skyenet.session.SocketManager import com.simiacryptus.skyenet.platform.AuthorizationManager.OperationType.GlobalKey import org.eclipse.jetty.websocket.api.Session import org.eclipse.jetty.websocket.api.WebSocketAdapter import org.slf4j.event.Level class ChatSocket( - val sessionId: String, - private val sessionState: SessionInterface, - val dataStorage: DataStorage?, - val user: AuthenticationManager.UserInfo?, + private val session: com.simiacryptus.skyenet.platform.Session, + private val sessionState: SocketManager, + private val dataStorage: DataStorage?, + private val user: User?, ) : WebSocketAdapter() { - private val userApi: OpenAIClient? - get() { - val user = user - val userSettings = if (user == null) null else ApplicationServices.userSettingsManager.getUserSettings(user.id) - return if (userSettings == null) null else { - if (userSettings.apiKey.isBlank()) null else object : OpenAIClient( - key = userSettings.apiKey, - logLevel = Level.DEBUG, - logStreams = mutableListOf( - dataStorage?.getSessionDir(user?.id, sessionId)?.resolve("openai.log")?.outputStream()?.buffered() - ).filterNotNull().toMutableList(), - ) { - override fun incrementTokens(model: OpenAIModel?, tokens: Usage) { - ApplicationServices.usageManager.incrementUsage(sessionId, user?.id, model!!, tokens) - super.incrementTokens(model, tokens) - } - } - } - } - + private val logfile = ".sys/openai.log" val api: OpenAIClient get() { val user = user val userApi = userApi if (userApi != null) return userApi - val canUseGlobalKey = authorizationManager.isAuthorized(null, user?.email, GlobalKey) + val canUseGlobalKey = authorizationManager.isAuthorized(null, user, GlobalKey) if (!canUseGlobalKey) throw RuntimeException("No API key") return object : OpenAIClient( logLevel = Level.DEBUG, logStreams = mutableListOf( - dataStorage?.getSessionDir(user?.id, sessionId)?.resolve("openai.log")?.outputStream()?.buffered() + dataStorage?.getSessionDir(user, session)?.resolve(logfile)?.outputStream()?.buffered() ).filterNotNull().toMutableList() ) { override fun incrementTokens(model: OpenAIModel?, tokens: Usage) { - if(null != model) ApplicationServices.usageManager.incrementUsage(sessionId, user?.id, model, tokens) + if(null != model) ApplicationServices.usageManager.incrementUsage(session, user, model, tokens) super.incrementTokens(model, tokens) } } } + private val userApi: OpenAIClient? + get() { + val user = user + val userSettings = if (user == null) null else ApplicationServices.userSettingsManager.getUserSettings(user) + return if (userSettings == null) null else { + if (userSettings.apiKey.isBlank()) null else object : OpenAIClient( + key = userSettings.apiKey, + logLevel = Level.DEBUG, + logStreams = mutableListOf( + dataStorage?.getSessionDir(user, session)?.resolve(logfile)?.outputStream()?.buffered() + ).filterNotNull().toMutableList(), + ) { + override fun incrementTokens(model: OpenAIModel?, tokens: Usage) { + ApplicationServices.usageManager.incrementUsage(session, user, model!!, tokens) + super.incrementTokens(model, tokens) + } + } + } + } + + override fun onWebSocketConnect(session: Session) { super.onWebSocketConnect(session) - log.debug("{} - Socket connected: {}", sessionId, session.remote) + log.debug("{} - Socket connected: {}", session, session.remote) sessionState.addSocket(this) sessionState.getReplay().forEach { try { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocketManager.kt similarity index 89% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocketManager.kt index a1d4596a..9529c766 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocketManager.kt @@ -5,20 +5,21 @@ import com.simiacryptus.openai.OpenAIClientBase.Companion.toContentList import com.simiacryptus.openai.models.ChatModels import com.simiacryptus.openai.models.OpenAITextModel import com.simiacryptus.skyenet.ApplicationBase -import com.simiacryptus.skyenet.session.SessionBase +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.session.SocketManagerBase import com.simiacryptus.skyenet.util.MarkdownUtil -open class ChatSession( +open class ChatSocketManager( val parent: ChatServer, - sessionId: String, + session: Session, val model: OpenAITextModel = ChatModels.GPT35Turbo, val userInterfacePrompt: String, - val initialAssistantPrompt: String = "", + private val initialAssistantPrompt: String = "", val systemPrompt: String, val api: OpenAIClient, val temperature: Double = 0.3, applicationClass: Class, -) : SessionBase(sessionId, parent.dataStorage, userId = null, applicationClass = applicationClass) { +) : SocketManagerBase(session, parent.dataStorage, userId = null, applicationClass = applicationClass) { init { if (userInterfacePrompt.isNotBlank()) { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/CodeChatServer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/CodeChatServer.kt index ca1be55d..42b053aa 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/CodeChatServer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/CodeChatServer.kt @@ -4,6 +4,9 @@ import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.ChatModels import com.simiacryptus.openai.models.OpenAITextModel import com.simiacryptus.skyenet.ApplicationBase +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User +import com.simiacryptus.skyenet.servlet.AppInfoServlet import com.simiacryptus.skyenet.util.ClasspathResource import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.util.resource.Resource @@ -20,8 +23,8 @@ class CodeChatServer( ) { override val applicationName: String get() = "Code Chat" - override fun newSession(userId: String?, sessionId: String) = object : ChatSession( - sessionId = sessionId, + override fun newSession(user: User?, session: Session) = object : ChatSocketManager( + session = session, parent = this@CodeChatServer, model = model, api = api, @@ -46,14 +49,14 @@ class CodeChatServer( """.trimMargin(), applicationClass = ApplicationBase::class.java, ) { - override fun canWrite(user: String?): Boolean = true + override fun canWrite(user: User?): Boolean = true } override val baseResource: Resource get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)!!) override fun configure(webAppContext: WebAppContext, path: String, baseUrl: String) { - webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/appInfo", AppInfoServlet()), "/appInfo") + webAppContext.addServlet(ServletHolder(javaClass.simpleName + "/appInfo", AppInfoServlet(applicationName)), "/appInfo") super.configure(webAppContext, path, baseUrl) } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AppInfoServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AppInfoServlet.kt new file mode 100644 index 00000000..0a6a690f --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AppInfoServlet.kt @@ -0,0 +1,20 @@ +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 AppInfoServlet(val applicationName:String) : 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 + ) + ) + ) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt index c6d65167..a2d4b731 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt @@ -9,8 +9,8 @@ import com.google.api.services.oauth2.Oauth2 import com.google.api.services.oauth2.model.Userinfo import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.platform.ApplicationServices -import com.simiacryptus.skyenet.platform.AuthenticationManager -import com.simiacryptus.skyenet.platform.AuthenticationManager.Companion.COOKIE_NAME +import com.simiacryptus.skyenet.platform.AuthenticationManager.Companion.AUTH_COOKIE +import com.simiacryptus.skyenet.platform.User import jakarta.servlet.* import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServlet @@ -37,12 +37,13 @@ open class AuthenticatedWebsite( open fun newUserSession(userInfo: Userinfo, sessionId: String) { log.info("User $userInfo logged in with session $sessionId") - ApplicationServices.authenticationManager.setUser(sessionId, AuthenticationManager.UserInfo( + ApplicationServices.authenticationManager.putUser(sessionId, User( id = userInfo.id, email = userInfo.email, name = userInfo.name, picture = userInfo.picture - )) + ) + ) } open fun configure(context: WebAppContext, addFilter: Boolean = true): WebAppContext { @@ -99,7 +100,7 @@ open class AuthenticatedWebsite( if (request is HttpServletRequest && response is HttpServletResponse) { if (isSecure(request)) { val sessionIdCookie = request.getCookie() - if (sessionIdCookie == null || !ApplicationServices.authenticationManager.containsKey(sessionIdCookie)) { + if (sessionIdCookie == null || !ApplicationServices.authenticationManager.containsUser(sessionIdCookie)) { response.sendRedirect("/googleLogin") return } @@ -122,7 +123,7 @@ open class AuthenticatedWebsite( val userInfo: Userinfo = oauth2.userinfo().get().execute() val sessionID = UUID.randomUUID().toString() newUserSession(userInfo, sessionID) - val sessionCookie = Cookie(COOKIE_NAME, sessionID) + val sessionCookie = Cookie(AUTH_COOKIE, sessionID) sessionCookie.path = "/" sessionCookie.isHttpOnly = true sessionCookie.secure = true diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt index 6574a2a3..1a763c62 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt @@ -4,6 +4,7 @@ import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.platform.ApplicationServices import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.Session import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -12,9 +13,9 @@ import java.io.File class FileServlet(val dataStorage: DataStorage) : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { val path = req.pathInfo ?: "/" - val pathSegments = Companion.parsePath(path) - val sessionID = pathSegments.first() - val sessionDir = dataStorage.getSessionDir(ApplicationServices.authenticationManager.getUser(req.getCookie())?.id, sessionID) + val pathSegments = parsePath(path) + val session = Session(pathSegments.first()) + val sessionDir = dataStorage.getSessionDir(ApplicationServices.authenticationManager.getUser(req.getCookie()), session) val filePath = pathSegments.drop(1).joinToString("/") val file = File(sessionDir, filePath) if (file.isFile) { @@ -29,10 +30,10 @@ class FileServlet(val dataStorage: DataStorage) : HttpServlet() { } else { resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK - val files = file.listFiles()?.filter { it.isFile }?.sortedBy { it.name }?.joinToString("
\n") { + val files = file.listFiles()?.filter { it.isFile && !it.name.startsWith(".") }?.sortedBy { it.name }?.joinToString("
\n") { """${it.name}""" } ?: "" - val folders = file.listFiles()?.filter { !it.isFile }?.sortedBy { it.name }?.joinToString("
\n") { + val folders = file.listFiles()?.filter { !it.isFile && !it.name.startsWith(".") }?.sortedBy { it.name }?.joinToString("
\n") { """${it.name}""" } ?: "" resp.writer.write( @@ -98,7 +99,7 @@ class FileServlet(val dataStorage: DataStorage) : HttpServlet() { | | |

Archive

- |ZIP + |ZIP |

Folders

|
|$folders diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt index 022fec7f..19d86a56 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt @@ -10,6 +10,6 @@ class NewSessionServlet : HttpServlet() { val sessionId = DataStorage.newGlobalID() resp.contentType = "text/plain" resp.status = HttpServletResponse.SC_OK - resp.writer.write(sessionId) + resp.writer.write(sessionId.toString()) } } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionListServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionListServlet.kt index c7c9027f..da47032d 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionListServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionListServlet.kt @@ -3,6 +3,7 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.platform.ApplicationServices import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.Session import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -14,7 +15,7 @@ class SessionListServlet( override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK - val sessions = dataStorage.listSessions(ApplicationServices.authenticationManager.getUser(req.getCookie())?.id) + val sessions = dataStorage.listSessions(ApplicationServices.authenticationManager.getUser(req.getCookie())) resp.writer.write( """ @@ -49,7 +50,7 @@ class SessionListServlet( ) } - private fun sessionName(req: HttpServletRequest, session: String) = dataStorage.getSessionName( - ApplicationServices.authenticationManager.getUser(req.getCookie())?.id, session + private fun sessionName(req: HttpServletRequest, session: Session) = dataStorage.getSessionName( + ApplicationServices.authenticationManager.getUser(req.getCookie()), session ) } \ 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 index 6020fb6d..daf1a631 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt @@ -3,6 +3,7 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.platform.ApplicationServices +import com.simiacryptus.skyenet.platform.Session import com.simiacryptus.util.JsonUtil import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -14,11 +15,11 @@ class SessionSettingsServlet( 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, ApplicationServices.authenticationManager.getUser( + if (req.parameterMap.containsKey("sessionId")) { + val session = Session(req.getParameter("sessionId")) + val settings = server.getSettings(session, ApplicationServices.authenticationManager.getUser( req.getCookie() - )?.id) + )) val json = if(settings != null) JsonUtil.toJson(settings) else "" //language=HTML resp.writer.write( @@ -30,7 +31,7 @@ class SessionSettingsServlet( | | |
- | + | | | | @@ -48,16 +49,17 @@ class SessionSettingsServlet( override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK - val sessionId = req.getParameter("sessionId") - if (null == sessionId) { + if (!req.parameterMap.containsKey("sessionId")) { resp.status = HttpServletResponse.SC_BAD_REQUEST resp.writer.write("Session ID is required") } else { + val session = Session(req.getParameter("sessionId")) val settings = JsonUtil.fromJson(req.getParameter("settings"), server.settingsClass) server.dataStorage.setJson( - ApplicationServices.authenticationManager.getUser(req.getCookie())?.id, - sessionId, settings, "settings.json") - resp.sendRedirect("${req.contextPath}/#$sessionId") + ApplicationServices.authenticationManager.getUser(req.getCookie()), + session, "settings.json", settings + ) + resp.sendRedirect("${req.contextPath}/#$session") } } } \ 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 index 6304a24c..9a69a607 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt @@ -5,6 +5,7 @@ import com.simiacryptus.openai.models.OpenAIModel import com.simiacryptus.openai.models.OpenAITextModel import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.platform.ApplicationServices +import com.simiacryptus.skyenet.platform.Session import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -14,15 +15,15 @@ class UsageServlet : HttpServlet() { resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK - val sessionId = req.getParameter("sessionId") - if (null != sessionId) { - serve(resp, ApplicationServices.usageManager.getSessionUsageSummary(sessionId)) + if (req.parameterMap.containsKey("sessionId")) { + val session = Session(req.getParameter("sessionId")) + serve(resp, ApplicationServices.usageManager.getSessionUsageSummary(session)) } else { val userinfo = ApplicationServices.authenticationManager.getUser(req.getCookie()) if (null == userinfo) { resp.status = HttpServletResponse.SC_BAD_REQUEST } else { - val usage = ApplicationServices.usageManager.getUserUsageSummary(userinfo.id) + val usage = ApplicationServices.usageManager.getUserUsageSummary(userinfo) serve(resp, usage) } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt index d99727c1..c18df3f2 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt @@ -16,7 +16,7 @@ class UserSettingsServlet : HttpServlet() { if (null == userinfo) { resp.status = HttpServletResponse.SC_BAD_REQUEST } else { - val settings = ApplicationServices.userSettingsManager.getUserSettings(userinfo.id) + val settings = ApplicationServices.userSettingsManager.getUserSettings(userinfo) val json = JsonUtil.toJson(settings) //language=HTML resp.writer.write( @@ -45,7 +45,7 @@ class UserSettingsServlet : HttpServlet() { resp.status = HttpServletResponse.SC_BAD_REQUEST } else { val settings = JsonUtil.fromJson(req.getParameter("settings"), UserSettings::class.java) - ApplicationServices.userSettingsManager.updateUserSettings(userinfo.id, settings) + ApplicationServices.userSettingsManager.updateUserSettings(userinfo, settings) resp.sendRedirect("/") } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/WelcomeServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/WelcomeServlet.kt index 667f6605..01a1b5a4 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/WelcomeServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/WelcomeServlet.kt @@ -3,10 +3,7 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.ApplicationDirectory -import com.simiacryptus.skyenet.platform.ApplicationServices -import com.simiacryptus.skyenet.platform.AuthenticationManager -import com.simiacryptus.skyenet.platform.AuthorizationManager -import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.* import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -47,7 +44,7 @@ open class WelcomeServlet(private val parent : ApplicationDirectory) : HttpServl } } - protected open fun homepage(user: AuthenticationManager.UserInfo?): String { + protected open fun homepage(user: User?): String { @Language("HTML") val html = """ @@ -79,12 +76,12 @@ open class WelcomeServlet(private val parent : ApplicationDirectory) : HttpServl parent.childWebApps.joinToString("\n") { app -> val canRun = ApplicationServices.authorizationManager.isAuthorized( applicationClass = app.server.javaClass, - user = user?.email, + user = user, operationType = AuthorizationManager.OperationType.Write ) val canRead = ApplicationServices.authorizationManager.isAuthorized( applicationClass = app.server.javaClass, - user = user?.email, + user = user, operationType = AuthorizationManager.OperationType.Read ) if (!canRead) return@joinToString "" @@ -114,6 +111,10 @@ open class WelcomeServlet(private val parent : ApplicationDirectory) : HttpServl } + + """ diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt index ff3016d2..91dcff6b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt @@ -3,6 +3,7 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie import com.simiacryptus.skyenet.platform.ApplicationServices import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.Session import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -12,11 +13,11 @@ import java.util.zip.ZipOutputStream class ZipServlet(val dataStorage: DataStorage) : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - val sessionID = req.getParameter("session") + val session = Session(req.getParameter("session")) val path = req.parameterMap.get("path")?.find { it.isNotBlank() } ?: "/" FileServlet.parsePath(path) // Validate path val sessionDir = dataStorage.getSessionDir( - ApplicationServices.authenticationManager.getUser(req.getCookie())?.id, sessionID) + ApplicationServices.authenticationManager.getUser(req.getCookie()), session) val file = File(sessionDir, path) val zipFile = File.createTempFile("skyenet", ".zip") try { @@ -41,9 +42,8 @@ class ZipServlet(val dataStorage: DataStorage) : HttpServlet() { zip.write(file.readBytes()) zip.closeEntry() } else { - file.listFiles()?.forEach { - write(basePath, it, zip) - } + file.listFiles()?.filter { !it.name.startsWith(".") } + ?.forEach { write(basePath, it, zip) } } } } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationInterface.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationInterface.kt new file mode 100644 index 00000000..8c2e49e2 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationInterface.kt @@ -0,0 +1,16 @@ +package com.simiacryptus.skyenet.session + +import java.util.function.Consumer + +class ApplicationInterface(private val inner: ApplicationSocketManager) { + fun send(html: String) = inner.send(html) + fun hrefLink(linkText: String, classname: String = """href-link""", handler: Consumer) = + inner.hrefLink(linkText, classname, handler) + + fun textInput(handler: Consumer): String = + inner.textInput(handler) + + fun newMessage(operationID: String, spinner: String, cancelable: Boolean = false): SessionMessage = + inner.newMessage(operationID, spinner, cancelable) + +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationSocketManager.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationSocketManager.kt new file mode 100644 index 00000000..4dc20b49 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/ApplicationSocketManager.kt @@ -0,0 +1,78 @@ +package com.simiacryptus.skyenet.session + +import com.simiacryptus.openai.OpenAIAPI +import com.simiacryptus.skyenet.ApplicationBase +import com.simiacryptus.skyenet.chat.ChatSocket +import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User +import java.util.function.Consumer + +abstract class ApplicationSocketManager( + session: Session, + userId: User?, + dataStorage: DataStorage?, + applicationClass: Class<*>, +) : SocketManagerBase( + session = session, + dataStorage = dataStorage, + userId = userId, + applicationClass = applicationClass, +) { + private val threads = mutableMapOf() + private val linkTriggers = mutableMapOf>() + private val txtTriggers = mutableMapOf>() + + override fun onRun(userMessage: String, socket: ChatSocket) { + val operationID = randomID() + val sessionDiv = newMessage(operationID, spinner, true) + threads[operationID] = Thread.currentThread() + newSession(session, user = userId, userMessage, this, sessionDiv, socket.api) + } + + val applicationInterface by lazy { ApplicationInterface(this) } + + override fun onCmd(id: String, code: String, socket: ChatSocket) { + if (code == "cancel") { + threads[id]?.interrupt() + } else if (code == "link") { + val consumer = linkTriggers[id] + consumer ?: throw IllegalArgumentException("No link handler found") + consumer.accept(Unit) + } else { + throw IllegalArgumentException("Unknown command: $code") + } + } + + fun hrefLink(linkText: String, classname: String = """href-link""", handler: Consumer): String { + val operationID = randomID() + linkTriggers[operationID] = handler + return """$linkText""" + } + + fun textInput(handler: Consumer): String { + val operationID = randomID() + txtTriggers[operationID] = handler + //language=HTML + return """ + + + """.trimIndent() + } + + abstract fun newSession( + session: Session, + user: User?, + userMessage: String, + socketManager: ApplicationSocketManager, + sessionMessage: SessionMessage, + api: OpenAIAPI + ) + + companion object { + val spinner: String get() = """
${ApplicationBase.spinner}
""" +// val playButton: String get() = """""" +// val cancelButton: String get() = """""" +// val regenButton: String get() = """""" + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDiv.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionMessage.kt similarity index 55% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDiv.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionMessage.kt index cbdf9037..bf280b11 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDiv.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionMessage.kt @@ -1,7 +1,9 @@ package com.simiacryptus.skyenet.session -abstract class SessionDiv { +import com.simiacryptus.skyenet.platform.Session + +abstract class SessionMessage { abstract fun append(htmlToAppend: String, showSpinner: Boolean) : Unit - abstract fun sessionID(): String + abstract fun sessionID(): Session abstract fun divID(): String } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionInterface.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SocketManager.kt similarity index 90% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionInterface.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/session/SocketManager.kt index 8332f71d..a3693d10 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionInterface.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SocketManager.kt @@ -2,7 +2,7 @@ package com.simiacryptus.skyenet.session import com.simiacryptus.skyenet.chat.ChatSocket -interface SessionInterface { +interface SocketManager { fun removeSocket(socket: ChatSocket) fun addSocket(socket: ChatSocket) fun getReplay(): List diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SocketManagerBase.kt similarity index 75% rename from webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt rename to webui/src/main/kotlin/com/simiacryptus/skyenet/session/SocketManagerBase.kt index a3b1423d..95957a11 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SocketManagerBase.kt @@ -1,26 +1,25 @@ package com.simiacryptus.skyenet.session import com.google.common.util.concurrent.MoreExecutors -import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.chat.ChatServer import com.simiacryptus.skyenet.chat.ChatSocket -import com.simiacryptus.skyenet.platform.ApplicationServices -import com.simiacryptus.skyenet.platform.AuthorizationManager -import com.simiacryptus.skyenet.platform.DataStorage +import com.simiacryptus.skyenet.platform.* import com.simiacryptus.skyenet.util.MarkdownUtil import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger -abstract class SessionBase( - val sessionId: String, +abstract class SocketManagerBase( + protected val session: Session, private val dataStorage: DataStorage?, - val userId: String? = null, + protected val userId: User? = null, private val messageStates: LinkedHashMap = dataStorage?.getMessages( - userId, sessionId + userId, session ) ?: LinkedHashMap(), - val applicationClass: Class, -) : SessionInterface { + private val applicationClass: Class<*>, +) : SocketManager { private val sockets: MutableSet = mutableSetOf() + private val messageVersions = HashMap() + protected open val pool = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()) override fun removeSocket(socket: ChatSocket) { sockets.remove(socket) @@ -30,7 +29,7 @@ abstract class SessionBase( sockets.add(socket) } - protected fun publish( + private fun publish( out: String, ) { val socketsSnapshot = sockets.toTypedArray() @@ -43,22 +42,22 @@ abstract class SessionBase( } } - fun newSessionDiv( + fun newMessage( operationID: String, spinner: String, cancelable: Boolean = false - ): SessionDiv { + ): SessionMessage { var responseContents = divInitializer(operationID, cancelable) send(responseContents) - return object : SessionDiv() { + return object : SessionMessage() { override fun append(htmlToAppend: String, showSpinner: Boolean) { if (htmlToAppend.isNotBlank()) { responseContents += """
$htmlToAppend
""" } val spinner1 = if (showSpinner) """
$spinner
""" else "" - return this@SessionBase.send("""$responseContents$spinner1""") + return this@SocketManagerBase.send("""$responseContents$spinner1""") } - override fun sessionID(): String { - return this@SessionBase.sessionId + override fun sessionID(): Session { + return this@SocketManagerBase.session } override fun divID(): String { @@ -67,38 +66,33 @@ abstract class SessionBase( } } - private val messageVersions = HashMap() - - fun send(out: String) { try { - log.debug("Send Msg: $sessionId - $out") + log.debug("Send Msg: $session - $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) + log.debug("$session - $out", e) } } - private fun setMessage(key: String, value: String): Int { - if (messageStates.containsKey(key) && messageStates[key] == value) return -1 - dataStorage?.updateMessage(userId, sessionId, key, value) - messageStates.put(key, value) - return messageVersions.computeIfAbsent(key) { AtomicInteger(0) }.incrementAndGet() - } - final 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()) + private fun setMessage(key: String, value: String): Int { + if (messageStates.containsKey(key) && messageStates[key] == value) return -1 + dataStorage?.updateMessage(userId, session, key, value) + messageStates.put(key, value) + return messageVersions.computeIfAbsent(key) { AtomicInteger(0) }.incrementAndGet() + } final override fun onWebSocketText(socket: ChatSocket, message: String) { - if (canWrite(socket.user?.email)) pool.submit { - log.debug("$sessionId - Received message: $message") + if (canWrite(userId)) pool.submit { + log.debug("$session - Received message: $message") try { val opCmdPattern = """![a-z]{3,7},.*""".toRegex() if (opCmdPattern.matches(message)) { @@ -109,16 +103,16 @@ abstract class SessionBase( onRun(message, socket) } } catch (e: Exception) { - log.warn("$sessionId - Error processing message: $message", e) + log.warn("$session - Error processing message: $message", e) send("""${randomID()},
${MarkdownUtil.renderMarkdown(e.message ?: "")}
""") } } else { - log.warn("$sessionId - Unauthorized message: $message") + log.warn("$session - Unauthorized message: $message") send("""${randomID()},
Unauthorized message
""") } } - open fun canWrite(user: String?) = ApplicationServices.authorizationManager.isAuthorized( + open fun canWrite(user: User?) = ApplicationServices.authorizationManager.isAuthorized( applicationClass = applicationClass, user = user, operationType = AuthorizationManager.OperationType.Write diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt index acb3c14a..ef8e9e49 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt @@ -1,11 +1,10 @@ package com.simiacryptus.skyenet.test +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.actors.CodingActor -import com.simiacryptus.skyenet.chat.ChatSocket -import com.simiacryptus.skyenet.platform.ApplicationServices +import com.simiacryptus.skyenet.platform.* import com.simiacryptus.skyenet.session.* -import com.simiacryptus.skyenet.platform.AuthorizationManager import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.util.* @@ -18,25 +17,26 @@ open class CodingActorTestApp( applicationName = applicationName, temperature = temperature, ) { - override fun processMessage( - sessionId: String, + override fun newSession( + session: Session, + user: User?, userMessage: String, - session: ApplicationSession, - sessionDiv: SessionDiv, - socket: ChatSocket + socketManager: ApplicationInterface, + sessionMessage: SessionMessage, + api: OpenAIAPI ) { - sessionDiv.append("""
${renderMarkdown(userMessage)}
""", true) - val response = actor.answer(userMessage, api = socket.api) + sessionMessage.append("""
${renderMarkdown(userMessage)}
""", true) + val response = actor.answer(userMessage, api = api) val canPlay = ApplicationServices.authorizationManager.isAuthorized( this::class.java, - socket.user?.email, + user, AuthorizationManager.OperationType.Execute ) val playLink = if(!canPlay) "" else { - session.htmlTools(sessionDiv.divID()).hrefLink("â–¶", "href-link play-button") { - sessionDiv.append("""
Running...
""", true) + socketManager.hrefLink("â–¶", "href-link play-button") { + sessionMessage.append("""
Running...
""", true) val result = response.run() - sessionDiv.append( + sessionMessage.append( """ |
${result.resultValue}
|
${result.resultOutput}
@@ -44,7 +44,7 @@ open class CodingActorTestApp( ) } } - sessionDiv.append("""
${ + sessionMessage.append("""
${ renderMarkdown(""" |```${actor.interpreter.getLanguage().lowercase(Locale.getDefault())} |${response.getCode()} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/ParsedActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/ParsedActorTestApp.kt index 8b841249..a21fddd2 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/ParsedActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/ParsedActorTestApp.kt @@ -1,9 +1,12 @@ package com.simiacryptus.skyenet.test +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.actors.ParsedActor -import com.simiacryptus.skyenet.chat.ChatSocket -import com.simiacryptus.skyenet.session.SessionDiv +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User +import com.simiacryptus.skyenet.session.ApplicationInterface +import com.simiacryptus.skyenet.session.SessionMessage import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown import com.simiacryptus.util.JsonUtil import org.slf4j.LoggerFactory @@ -16,22 +19,23 @@ open class ParsedActorTestApp( applicationName = applicationName, temperature = temperature, ) { - override fun processMessage( - sessionId: String, + override fun newSession( + session: Session, + user: User?, userMessage: String, - session: ApplicationSession, - sessionDiv: SessionDiv, - socket: ChatSocket + socketManager: ApplicationInterface, + sessionMessage: SessionMessage, + api: OpenAIAPI ) { - sessionDiv.append("""
${renderMarkdown(userMessage)}
""", true) - val response = actor.answer(userMessage, api = socket.api) - sessionDiv.append( + sessionMessage.append("""
${renderMarkdown(userMessage)}
""", true) + val response = actor.answer(userMessage, api = api) + sessionMessage.append( """
${ renderMarkdown( """ |${response.getText()} |``` - |${JsonUtil.toJson(response.getObj()!!)} + |${JsonUtil.toJson(response.getObj())} |``` """.trimMargin().trim() ) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/SimpleActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/SimpleActorTestApp.kt index c84e7c1a..f551ec67 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/SimpleActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/SimpleActorTestApp.kt @@ -1,8 +1,10 @@ package com.simiacryptus.skyenet.test +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.actors.SimpleActor -import com.simiacryptus.skyenet.chat.ChatSocket +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User import com.simiacryptus.skyenet.session.* import com.simiacryptus.skyenet.util.MarkdownUtil import org.slf4j.LoggerFactory @@ -20,19 +22,20 @@ open class SimpleActorTestApp( 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 + @Suppress("UNCHECKED_CAST") override fun initSettings(session: Session): T? = Settings(actor=actor) as T - override fun processMessage( - sessionId: String, + override fun newSession( + session: Session, + user: User?, userMessage: String, - session: ApplicationSession, - sessionDiv: SessionDiv, - socket: ChatSocket + socketManager: ApplicationInterface, + sessionMessage: SessionMessage, + api: OpenAIAPI ) { - val actor = getSettings(sessionId, session.userId)?.actor ?: actor - sessionDiv.append("""
${MarkdownUtil.renderMarkdown(userMessage)}
""", true) - val moderatorResponse = actor.answer(userMessage, api = socket.api) - sessionDiv.append("""
${MarkdownUtil.renderMarkdown(moderatorResponse)}
""", false) + val actor = getSettings(session, user)?.actor ?: actor + sessionMessage.append("""
${MarkdownUtil.renderMarkdown(userMessage)}
""", true) + val moderatorResponse = actor.answer(userMessage, api = api) + sessionMessage.append("""
${MarkdownUtil.renderMarkdown(moderatorResponse)}
""", false) } companion object { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/EmbeddingVisualizer.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/EmbeddingVisualizer.kt index 62b1d7a9..84bf5cc4 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/EmbeddingVisualizer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/EmbeddingVisualizer.kt @@ -1,23 +1,27 @@ package com.simiacryptus.skyenet.util +import com.simiacryptus.openai.OpenAIAPI import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.openai.models.EmbeddingModels import com.simiacryptus.skyenet.platform.DataStorage -import com.simiacryptus.skyenet.session.SessionBase +import com.simiacryptus.skyenet.platform.Session +import com.simiacryptus.skyenet.platform.User +import com.simiacryptus.skyenet.session.ApplicationInterface import com.simiacryptus.util.JsonUtil class EmbeddingVisualizer( - val api: OpenAIClient, + val api: OpenAIAPI, val dataStorage: DataStorage, - val sessionID: String, + val sessionID: Session, val appPath: String, val host: String, - val session: SessionBase, + val session: ApplicationInterface, + val userId: User?, ) { private fun toVectorMap(vararg words: String): Map { val vectors = words.map {word -> - word to api.createEmbedding( + word to (api as OpenAIClient).createEmbedding( OpenAIClient.EmbeddingRequest( model = EmbeddingModels.AdaEmbedding.modelName, input = word, @@ -42,8 +46,8 @@ class EmbeddingVisualizer( val vectorFileName = "vectors.tsv" val metadataFileName = "metadata.tsv" val configFileName = "projector-config.json" - dataStorage.getSessionDir(session.userId, sessionID).resolve(vectorFileName).writeText(vectorTsv) - dataStorage.getSessionDir(session.userId, sessionID).resolve(metadataFileName).writeText(metadataTsv) + dataStorage.getSessionDir(userId, sessionID).resolve(vectorFileName).writeText(vectorTsv) + dataStorage.getSessionDir(userId, sessionID).resolve(metadataFileName).writeText(metadataTsv) // projector-config.json val projectorConfig = JsonUtil.toJson( mapOf( @@ -57,7 +61,7 @@ class EmbeddingVisualizer( ) ) ) - dataStorage.getSessionDir(session.userId, sessionID).resolve(configFileName).writeText(projectorConfig) + dataStorage.getSessionDir(userId, sessionID).resolve(configFileName).writeText(projectorConfig) return """ Projector Config Vectors diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt deleted file mode 100644 index 16d4aaa2..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/HtmlTools.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.simiacryptus.skyenet.util - -import com.simiacryptus.skyenet.ApplicationBase -import com.simiacryptus.skyenet.session.SessionBase -import java.util.function.Consumer - -class HtmlTools( - val app: ApplicationBase.ApplicationSession, - private val operationID: String = SessionBase.randomID() -) { - val spinner: String get() = """
${ApplicationBase.spinner}
""" - val playButton: String get() = """""" - val cancelButton: String get() = """""" - val regenButton: String get() = """""" - - private val txtTriggers = mutableMapOf>() - fun hrefLink(linkText : String, classname: String = """href-link""", handler: Consumer): String { - val operationID = SessionBase.randomID() - app.linkTriggers[operationID] = handler - return """$linkText""" - } - fun textInput(handler: Consumer): String { - val operationID = SessionBase.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/MutableSessionHandler.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MutableSessionHandler.kt index 6f92ff37..0b6e1304 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MutableSessionHandler.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MutableSessionHandler.kt @@ -1,13 +1,13 @@ package com.simiacryptus.skyenet.util import com.simiacryptus.skyenet.chat.ChatSocket -import com.simiacryptus.skyenet.session.SessionInterface +import com.simiacryptus.skyenet.session.SocketManager -class MutableSessionHandler(initialDelegate: SessionInterface?) : SessionInterface { - private var priorDelegates: MutableList = mutableListOf() - private var currentDelegate: SessionInterface? = initialDelegate +class MutableSessionHandler(initialDelegate: SocketManager?) : SocketManager { + private var priorDelegates: MutableList = mutableListOf() + private var currentDelegate: SocketManager? = initialDelegate - fun setDelegate(delegate: SessionInterface) { + fun setDelegate(delegate: SocketManager) { if(null != currentDelegate) priorDelegates.add(currentDelegate!!) currentDelegate = delegate for (socket in sockets) { diff --git a/webui/src/main/resources/codeChat/chat.css b/webui/src/main/resources/codeChat/chat.css index 380271af..1b0486b6 100644 --- a/webui/src/main/resources/codeChat/chat.css +++ b/webui/src/main/resources/codeChat/chat.css @@ -15,8 +15,8 @@ body { color: var(--primary-text-color); background-color: var(--primary-bg-color); margin: 0; - padding: 0; overflow: clip; + padding: 0 0 50px; } #messages { @@ -289,3 +289,22 @@ pre { font-weight: bold; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } + + +#footer { + position: fixed; + bottom: 0; + width: 100%; + text-align: right; + z-index: 1000; +} + +#footer a { + color: darkgray; + text-decoration: none; + font-weight: bold; +} + +#footer a:hover { + text-decoration: underline; +} diff --git a/webui/src/main/resources/codeChat/index.html b/webui/src/main/resources/codeChat/index.html index 7a626dcb..54abcedb 100644 --- a/webui/src/main/resources/codeChat/index.html +++ b/webui/src/main/resources/codeChat/index.html @@ -37,6 +37,10 @@
+ + diff --git a/webui/src/main/resources/simpleSession/chat.css b/webui/src/main/resources/simpleSession/chat.css index 98f9881e..9cfd5154 100644 --- a/webui/src/main/resources/simpleSession/chat.css +++ b/webui/src/main/resources/simpleSession/chat.css @@ -15,7 +15,7 @@ body { color: var(--primary-text-color); background-color: var(--primary-bg-color); margin: 0; - padding: 0; + padding: 0 0 50px; } #messages { @@ -300,3 +300,21 @@ pre.verbose, pre.response-message { margin-top: 20px; margin-bottom: 10px; } + +#footer { + position: fixed; + bottom: 0; + width: 100%; + text-align: right; + z-index: 1000; +} + +#footer a { + color: darkgray; + text-decoration: none; + font-weight: bold; +} + +#footer a:hover { + text-decoration: underline; +} diff --git a/webui/src/main/resources/simpleSession/index.html b/webui/src/main/resources/simpleSession/index.html index f8381a9d..99e4b719 100644 --- a/webui/src/main/resources/simpleSession/index.html +++ b/webui/src/main/resources/simpleSession/index.html @@ -28,7 +28,7 @@ Session Settings Files Usage - Hide Verbose + Show Verbose
@@ -50,6 +50,10 @@
+ + diff --git a/webui/src/main/resources/welcome/chat.css b/webui/src/main/resources/welcome/chat.css index 59411d60..e922a7ec 100644 --- a/webui/src/main/resources/welcome/chat.css +++ b/webui/src/main/resources/welcome/chat.css @@ -1,3 +1,23 @@ +:root { + --primary-bg-color: #f7f7f7; + --secondary-bg-color: #ffffff; + --primary-text-color: #333; + --secondary-text-color: #555; + --link-color: #0066cc; + --link-hover-color: #0044aa; + --border-radius: 4px; + --box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + --transition-speed: 0.3s; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--primary-text-color); + background-color: var(--primary-bg-color); + margin: 0; + padding: 0 0 50px; +} + #form { position: absolute; left: 0; @@ -132,3 +152,21 @@ pre { text-decoration: none; cursor: pointer; } + +#footer { + position: fixed; + bottom: 0; + width: 100%; + text-align: right; + z-index: 1000; +} + +#footer a { + color: darkgray; + text-decoration: none; + font-weight: bold; +} + +#footer a:hover { + text-decoration: underline; +} diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt index 4dc514ca..1781200e 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt @@ -9,6 +9,7 @@ import com.simiacryptus.skyenet.platform.AuthorizationManager import com.simiacryptus.skyenet.heart.GroovyInterpreter import com.simiacryptus.skyenet.heart.KotlinInterpreter import com.simiacryptus.skyenet.heart.ScalaLocalInterpreter +import com.simiacryptus.skyenet.platform.User import com.simiacryptus.skyenet.test.CodingActorTestApp import com.simiacryptus.skyenet.test.ParsedActorTestApp import com.simiacryptus.skyenet.test.SimpleActorTestApp @@ -36,7 +37,7 @@ object ActorTestAppServer : ApplicationDirectory(port = 8082) { @JvmStatic fun main(args: Array) { - val mockUser = AuthenticationManager.UserInfo( + val mockUser = User( "1", "user@mock.test", "Test User", @@ -44,13 +45,13 @@ object ActorTestAppServer : ApplicationDirectory(port = 8082) { ) ApplicationServices.authenticationManager = object : AuthenticationManager() { override fun getUser(sessionId: String?) = mockUser - override fun containsKey(value: String) = true - override fun setUser(sessionId: String, userInfo: UserInfo) = throw UnsupportedOperationException() + override fun containsUser(value: String) = true + override fun putUser(sessionId: String, user: User) = throw UnsupportedOperationException() } ApplicationServices.authorizationManager = object : AuthorizationManager() { override fun isAuthorized( applicationClass: Class<*>?, - user: String?, + user: User?, operationType: OperationType ): Boolean = true }