From 304d5c8fda5b0aa5c89c57804700684c209be999 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Tue, 14 Nov 2023 23:13:40 -0500 Subject: [PATCH] 1.0.30 (#34) * 1.0.30 * Show errors --- README.md | 6 +- core/build.gradle.kts | 2 + .../kotlin/com/simiacryptus/skyenet/Brain.kt | 26 --- .../skyenet/actors/CodingActor.kt | 51 ++++- .../skyenet/config/ApplicationServices.kt | 37 ++++ .../skyenet/config/AuthenticationManager.kt | 28 +++ .../{util => config}/AuthorizationManager.kt | 6 +- .../skyenet/config/DataStorage.kt | 163 ++++++++++++++++ .../skyenet/{util => config}/UsageManager.kt | 32 +-- .../{util => config}/UserSettingsManager.kt | 17 +- .../skyenet/util/SessionServerUtil.kt | 56 ------ .../simiacryptus/skyenet/DataStorageTest.kt | 14 +- gradle.properties | 2 +- .../simiacryptus/skyenet/ApplicationBase.kt | 66 +++---- .../skyenet/ApplicationDirectory.kt | 184 ++++++------------ .../simiacryptus/skyenet/chat/ChatServer.kt | 15 +- .../simiacryptus/skyenet/chat/ChatSession.kt | 2 +- .../simiacryptus/skyenet/chat/ChatSocket.kt | 30 ++- .../skyenet/servlet/AuthenticatedWebsite.kt | 32 +-- .../skyenet/servlet/FileServlet.kt | 10 +- .../skyenet/servlet/NewSessionServlet.kt | 4 +- .../skyenet/servlet/SessionServlet.kt | 18 +- .../skyenet/servlet/SessionSettingsServlet.kt | 10 +- .../skyenet/servlet/UsageServlet.kt | 11 +- .../skyenet/servlet/UserInfoServlet.kt | 5 +- .../skyenet/servlet/UserSettingsServlet.kt | 15 +- .../skyenet/servlet/WelcomeServlet.kt | 117 +++++++++++ .../skyenet/servlet/ZipServlet.kt | 12 +- .../skyenet/session/SessionBase.kt | 18 +- .../skyenet/session/SessionDataStorage.kt | 161 --------------- .../skyenet/test/CodingActorTestApp.kt | 8 +- .../skyenet/test/ParsedActorTestApp.kt | 1 - .../skyenet/test/SimpleActorTestApp.kt | 3 +- .../skyenet/util/EmbeddingVisualizer.kt | 12 +- .../src/main/resources/simpleSession/chat.css | 12 ++ .../main/resources/simpleSession/index.html | 1 + .../src/main/resources/simpleSession/main.js | 20 ++ .../skyenet/ActorTestAppServer.kt | 21 ++ 38 files changed, 700 insertions(+), 528 deletions(-) create mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/config/ApplicationServices.kt create mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthenticationManager.kt rename core/src/main/kotlin/com/simiacryptus/skyenet/{util => config}/AuthorizationManager.kt (94%) create mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/config/DataStorage.kt rename core/src/main/kotlin/com/simiacryptus/skyenet/{util => config}/UsageManager.kt (81%) rename core/src/main/kotlin/com/simiacryptus/skyenet/{util => config}/UserSettingsManager.kt (60%) delete mode 100644 core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt rename webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt => core/src/test/java/com/simiacryptus/skyenet/DataStorageTest.kt (68%) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/WelcomeServlet.kt delete mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDataStorage.kt diff --git a/README.md b/README.md index 1ba7d398..4de53bdc 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.29 + 1.0.30 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.29' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.30' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.29") +implementation("com.simiacryptus:skyenet:1.0.30") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f81bdaca..d02bc8c8 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -55,6 +55,8 @@ dependencies { testImplementation(group = "com.amazonaws", name = "aws-java-sdk", version = "1.12.587") testImplementation(group = "ch.qos.logback", name = "logback-classic", version = logback_version) testImplementation(group = "ch.qos.logback", name = "logback-core", version = logback_version) + //mockito + testImplementation(group = "org.mockito", name = "mockito-core", version = "5.7.0") } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt index 1d11e722..75d60a0e 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/Brain.kt @@ -60,32 +60,6 @@ open class Brain( ) ) - open fun fixCommand( - previousCode: String, - error: Throwable, - output: String, - vararg prompt: String - ): Pair>> { - val promptMessages = listOf( - ChatMessage( - ChatMessage.Role.system, """ - |You will translate natural language instructions into - |an implementation using $language and the script context. - |Use ``` code blocks labeled with $language where appropriate. - |Defined symbols include ${symbols.keySet().joinToString(", ")}. - |Do not include wrapping code blocks, assume a REPL context. - |The runtime context is described below: - | - |$apiDescription - |""".trimMargin().trim() - ) - ) + prompt.map { - ChatMessage(ChatMessage.Role.user, it) - } - if (verbose) log.info("Prompt: \n\t" + prompt.joinToString("\n\t")) - return fixCommand(previousCode, error, output, *promptMessages.toTypedArray()) - } - fun fixCommand( previousCode: String, error: Throwable, 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 011ac62b..0f39b66e 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/actors/CodingActor.kt @@ -6,9 +6,6 @@ import com.simiacryptus.skyenet.Brain.Companion.indent import com.simiacryptus.skyenet.Brain.Companion.superMethod import com.simiacryptus.skyenet.Heart import com.simiacryptus.skyenet.OutputInterceptor -import com.simiacryptus.skyenet.util.SessionServerUtil.asJava -import com.simiacryptus.skyenet.util.SessionServerUtil.getCode -import com.simiacryptus.skyenet.util.SessionServerUtil.getRenderedResponse import com.simiacryptus.util.describe.AbbrevWhitelistYamlDescriber import java.lang.reflect.Modifier import kotlin.reflect.KClass @@ -194,6 +191,54 @@ open class CodingActor( companion object { val log = org.slf4j.LoggerFactory.getLogger(CodingActor::class.java) + fun getRenderedResponse(respondWithCode: List>) = + respondWithCode.joinToString("\n") { + var language = it.first + if (language == "code") language = "groovy" + if (language == "text") { + //language=HTML + """ + |
+ |${it.second} + |
+ |""".trimMargin().trim() + } else { + //language=HTML + """ + |

+                    |${it.second}
+                    |
+ |""".trimMargin().trim() + } + } + + fun getCode(language: String, textSegments: List>) = + textSegments.joinToString("\n") { + if (it.first.lowercase() == "code" || it.first.lowercase() == language.lowercase()) { + """ + |${it.second} + |""".trimMargin().trim() + } else { + "" + } + } + + operator fun java.util.Map.plus(mapOf: Map): java.util.Map { + val hashMap = java.util.HashMap() + this.forEach(hashMap::put) + hashMap.putAll(mapOf) + return hashMap as java.util.Map + } + + val Map.asJava: java.util.Map + get() { + return java.util.HashMap().also { map -> + this.forEach { (key, value) -> + map[key] = value + } + } as java.util.Map + } + } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/config/ApplicationServices.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/config/ApplicationServices.kt new file mode 100644 index 00000000..2ad5f7cc --- /dev/null +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/config/ApplicationServices.kt @@ -0,0 +1,37 @@ +package com.simiacryptus.skyenet.config + +import java.io.File + +object ApplicationServices { + var isLocked: Boolean = false + set(value) { + require(!isLocked) { "ApplicationServices is locked" } + field = value + } + var usageManager: UsageManager = UsageManager() + set(value) { + require(!isLocked) { "ApplicationServices is locked" } + field = value + } + var authorizationManager: AuthorizationManager = AuthorizationManager() + set(value) { + require(!isLocked) { "ApplicationServices is locked" } + field = value + } + var userSettingsManager: UserSettingsManager = UserSettingsManager() + set(value) { + require(!isLocked) { "ApplicationServices is locked" } + field = value + } + var authenticationManager: AuthenticationManager = AuthenticationManager() + set(value) { + require(!isLocked) { "ApplicationServices is locked" } + field = value + } + var dataStorageFactory: (File) -> DataStorage = { DataStorage(it) } + set(value) { + require(!isLocked) { "ApplicationServices is locked" } + field = value + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthenticationManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthenticationManager.kt new file mode 100644 index 00000000..14d89b6c --- /dev/null +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthenticationManager.kt @@ -0,0 +1,28 @@ +package com.simiacryptus.skyenet.config + +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() + + open fun getUser(sessionId: String?) = if (null == sessionId) null else users[sessionId] + + open fun containsKey(value: String): Boolean = users.containsKey(value) + + open fun setUser(sessionId: String, userInfo: UserInfo) { + users[sessionId] = userInfo + } + + companion object { + const val COOKIE_NAME = "sessionId" + private val log = org.slf4j.LoggerFactory.getLogger(AuthenticationManager::class.java) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/util/AuthorizationManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthorizationManager.kt similarity index 94% rename from core/src/main/kotlin/com/simiacryptus/skyenet/util/AuthorizationManager.kt rename to core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthorizationManager.kt index 517be110..4f186cf4 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/util/AuthorizationManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/config/AuthorizationManager.kt @@ -1,8 +1,8 @@ -package com.simiacryptus.skyenet.util +package com.simiacryptus.skyenet.config import java.util.* -object AuthorizationManager { +open class AuthorizationManager { enum class OperationType { Read, @@ -12,7 +12,7 @@ object AuthorizationManager { GlobalKey, } - fun isAuthorized( + open fun isAuthorized( applicationClass: Class<*>?, user: String?, operationType: OperationType, diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/config/DataStorage.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/config/DataStorage.kt new file mode 100644 index 00000000..7c0f08c7 --- /dev/null +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/config/DataStorage.kt @@ -0,0 +1,163 @@ +package com.simiacryptus.skyenet.config + +import com.simiacryptus.util.JsonUtil +import java.io.File +import java.util.* + + +open class DataStorage( + 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 getMessages(userId: String?, sessionId: String): LinkedHashMap { + validateSessionId(sessionId) + val messageDir = File(this.getSessionDir(userId, sessionId), MESSAGE_DIR) + val messages = LinkedHashMap() + log.debug("Loading messages for $sessionId: ${messageDir.absolutePath}") + messageDir.listFiles()?.sortedBy { it.lastModified() }?.forEach { file -> + val message = JsonUtil.objectMapper().readValue(file, String::class.java) + messages[file.nameWithoutExtension] = message + } + log.debug("Loaded ${messages.size} messages for $sessionId") + return messages + } + + 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" } + } + + 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() + } + + open fun getSessionName(userId: String?, sessionId: String): String { + validateSessionId(sessionId) + val userMessage = File(this.getSessionDir(userId, sessionId), 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") + "" + } else { + val stringList = split[1].split("

") + if (stringList.isEmpty()) { + log.debug("Session $sessionId: No messages") + "" + } else { + stringList.first() + } + } + }?.first { it.isNotEmpty() } + return if (null != userMessage) { + log.debug("Session $sessionId: $userMessage") + userMessage + } else { + log.debug("Session $sessionId: No messages") + 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 setJson(userId: String?, sessionId: String, settings: T, filename: String): T { + validateSessionId(sessionId) + val settingsFile = File(this.getSessionDir(userId, sessionId), 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 + } + + else -> { + throw IllegalArgumentException("Invalid session ID: $sessionId") + } + } + } + + private fun userRoot(userId: String?) = File( + File(dataDir, "users"), + userId ?: throw IllegalArgumentException("User ID 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 { + 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" + } + + fun newUserID(): String { + 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" + } + + private const val MESSAGE_DIR = "messages" + fun String.stripPrefix(prefix: String) = if (!this.startsWith(prefix)) this else { + this.substring(prefix.length) + } + + } +} diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/util/UsageManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/config/UsageManager.kt similarity index 81% rename from core/src/main/kotlin/com/simiacryptus/skyenet/util/UsageManager.kt rename to core/src/main/kotlin/com/simiacryptus/skyenet/config/UsageManager.kt index b59e251e..8f404398 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/util/UsageManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/config/UsageManager.kt @@ -1,4 +1,4 @@ -package com.simiacryptus.skyenet.util +package com.simiacryptus.skyenet.config import com.simiacryptus.openai.OpenAIClient import com.simiacryptus.util.JsonUtil @@ -8,12 +8,11 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -object UsageManager { - val log = org.slf4j.LoggerFactory.getLogger(UsageManager::class.java) +open class UsageManager { private val scheduler = Executors.newSingleThreadScheduledExecutor() private val txLogFile = File(".skyenet/usage/log.csv") - @Volatile private var txLogFileWriter: FileWriter + @Volatile private var txLogFileWriter: FileWriter? private val usagePerSession = HashMap() private val sessionsByUser = HashMap>() private val usersBySession = HashMap>() @@ -26,7 +25,7 @@ object UsageManager { } @Suppress("MemberVisibilityCanBePrivate") - fun loadFromLog(file: File) { + open fun loadFromLog(file: File) { if (file.exists()) { file.readLines().forEach { line -> val (sessionId, user, model, tokens) = line.split(",") @@ -37,7 +36,7 @@ object UsageManager { } @Suppress("MemberVisibilityCanBePrivate") - fun writeCompactLog(file: File) { + open fun writeCompactLog(file: File) { val writer = FileWriter(file) usagePerSession.forEach { (sessionId, usage) -> val user = usersBySession[sessionId]?.firstOrNull() @@ -56,7 +55,7 @@ object UsageManager { val swapFile = File(txLogFile.absolutePath + ".old") synchronized(txLogFile) { try { - txLogFileWriter.close() + txLogFileWriter?.close() } catch (e: Exception) { log.warn("Error closing log file", e) } @@ -80,7 +79,7 @@ object UsageManager { File(".skyenet/usage/counters.json").writeText(JsonUtil.toJson(usagePerSession)) } - fun incrementUsage(sessionId: String, user: String?, model: OpenAIClient.Model, tokens: Int) { + open fun incrementUsage(sessionId: String, user: String?, model: OpenAIClient.Model, tokens: Int) { val usage = usagePerSession.getOrPut(sessionId) { UsageCounters() } @@ -95,16 +94,19 @@ object UsageManager { sessions.add(sessionId) } try { - synchronized(txLogFile) { - txLogFileWriter.write("$sessionId,$user,${model.modelName},$tokens\n") - txLogFileWriter.flush() + val txLogFileWriter = txLogFileWriter + if(null != txLogFileWriter) { + synchronized(txLogFile) { + txLogFileWriter.write("$sessionId,$user,${model.modelName},$tokens\n") + txLogFileWriter.flush() + } } } catch (e: Exception) { log.warn("Error incrementing usage", e) } } - fun getUserUsageSummary(user: String): Map { + open fun getUserUsageSummary(user: String): Map { val sessions = sessionsByUser[user] return sessions?.flatMap { sessionId -> val usage = usagePerSession[sessionId] @@ -114,7 +116,7 @@ object UsageManager { }?.groupBy { it.first }?.mapValues { it.value.map { it.second }.sum() } ?: emptyMap() } - fun getSessionUsageSummary(sessionId: String): Map { + open fun getSessionUsageSummary(sessionId: String): Map { val usage = usagePerSession[sessionId] return usage?.tokensPerModel?.entries?.map { (model, counter) -> model to counter.get() @@ -124,4 +126,8 @@ object UsageManager { data class UsageCounters( val tokensPerModel: HashMap = HashMap(), ) + + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(UsageManager::class.java) + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/util/UserSettingsManager.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/config/UserSettingsManager.kt similarity index 60% rename from core/src/main/kotlin/com/simiacryptus/skyenet/util/UserSettingsManager.kt rename to core/src/main/kotlin/com/simiacryptus/skyenet/config/UserSettingsManager.kt index 8f69d878..7aee7390 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/util/UserSettingsManager.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/config/UserSettingsManager.kt @@ -1,35 +1,38 @@ -package com.simiacryptus.skyenet.util +package com.simiacryptus.skyenet.config import com.simiacryptus.util.JsonUtil import java.io.File -object UserSettingsManager { +open class UserSettingsManager { data class UserSettings( val apiKey: String = "", ) - private val log = org.slf4j.LoggerFactory.getLogger(UserSettingsManager::class.java) private val userSettings = HashMap() private val userConfigDirectory = File(".skyenet/users") - fun getUserSettings(user: String): UserSettings { + open fun getUserSettings(user: String): UserSettings { return userSettings.getOrPut(user) { val file = File(userConfigDirectory, "$user.json") if (file.exists()) { - log.info("Loading user settings for $user from $file") + Companion.log.info("Loading user settings for $user from $file") JsonUtil.fromJson(file.readText(), UserSettings::class.java) } else { - log.info("Creating new user settings for $user at $file") + Companion.log.info("Creating new user settings for $user at $file") UserSettings() } } } - fun updateUserSettings(user: String, settings: UserSettings) { + open fun updateUserSettings(user: String, settings: UserSettings) { userSettings[user] = settings val file = File(userConfigDirectory, "$user.json") file.parentFile.mkdirs() file.writeText(JsonUtil.toJson(settings)) } + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(UserSettingsManager::class.java) + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt deleted file mode 100644 index e9d4d5e2..00000000 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/util/SessionServerUtil.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.simiacryptus.skyenet.util - -import com.simiacryptus.openai.OpenAIClient.ChatMessage - -object SessionServerUtil { - fun getRenderedResponse(respondWithCode: List>) = - respondWithCode.joinToString("\n") { - var language = it.first - if (language == "code") language = "groovy" - if (language == "text") { - //language=HTML - """ - |
- |${it.second} - |
- |""".trimMargin().trim() - } else { - //language=HTML - """ - |

-                    |${it.second}
-                    |
- |""".trimMargin().trim() - } - } - - val log = org.slf4j.LoggerFactory.getLogger(SessionServerUtil::class.java) - - fun getCode(language: String, textSegments: List>) = - textSegments.joinToString("\n") { - if (it.first.lowercase() == "code" || it.first.lowercase() == language.lowercase()) { - """ - |${it.second} - |""".trimMargin().trim() - } else { - "" - } - } - - operator fun java.util.Map.plus(mapOf: Map): java.util.Map { - val hashMap = java.util.HashMap() - this.forEach(hashMap::put) - hashMap.putAll(mapOf) - return hashMap as java.util.Map - } - - val Map.asJava: java.util.Map - get() { - return java.util.HashMap().also { map -> - this.forEach { (key, value) -> - map[key] = value - } - } as java.util.Map - } - -} \ No newline at end of file diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt b/core/src/test/java/com/simiacryptus/skyenet/DataStorageTest.kt similarity index 68% rename from webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt rename to core/src/test/java/com/simiacryptus/skyenet/DataStorageTest.kt index f87b126d..7c3d6a04 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/SessionDataStorageTest.kt +++ b/core/src/test/java/com/simiacryptus/skyenet/DataStorageTest.kt @@ -1,6 +1,6 @@ package com.simiacryptus.skyenet -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.DataStorage import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -9,14 +9,14 @@ import org.junit.jupiter.api.Test import java.io.File import java.nio.file.Files -class SessionDataStorageTest { +class DataStorageTest { private lateinit var tempDir: File - private lateinit var storage: SessionDataStorage + private lateinit var storage: DataStorage @BeforeEach fun setUp() { tempDir = Files.createTempDirectory("sessionDataTest").toFile() - storage = SessionDataStorage(dataDir = tempDir) + storage = DataStorage(dataDir = tempDir) } @AfterEach @@ -26,12 +26,12 @@ class SessionDataStorageTest { @Test fun testUpdateAndLoadMessage() { - val sessionId = SessionDataStorage.newID() + val sessionId = DataStorage.newGlobalID() val messageId = "message1" val message = "This is a test message." - storage.updateMessage(sessionId, messageId, message) - val messages = storage.loadMessages(sessionId) + storage.updateMessage(null, sessionId, messageId, message) + val messages = storage.getMessages(null, sessionId) assertEquals(1, messages.size) assertTrue(messages.containsKey(messageId)) diff --git a/gradle.properties b/gradle.properties index c7da930c..39e7b34d 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.29 +libraryVersion = 1.0.30 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/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt index 7c241ef9..522bee3c 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationBase.kt @@ -2,19 +2,18 @@ package com.simiacryptus.skyenet import com.simiacryptus.skyenet.chat.ChatServer import com.simiacryptus.skyenet.chat.ChatSocket +import com.simiacryptus.skyenet.config.ApplicationServices.authenticationManager +import com.simiacryptus.skyenet.config.ApplicationServices.authorizationManager +import com.simiacryptus.skyenet.config.ApplicationServices.dataStorageFactory import com.simiacryptus.skyenet.servlet.* +import com.simiacryptus.skyenet.config.AuthenticationManager.Companion.COOKIE_NAME import com.simiacryptus.skyenet.session.SessionBase -import com.simiacryptus.skyenet.session.SessionDataStorage import com.simiacryptus.skyenet.session.SessionDiv import com.simiacryptus.skyenet.session.SessionInterface -import com.simiacryptus.skyenet.util.AuthorizationManager -import com.simiacryptus.skyenet.util.AuthorizationManager.isAuthorized +import com.simiacryptus.skyenet.config.AuthorizationManager import com.simiacryptus.skyenet.util.HtmlTools -import com.simiacryptus.util.JsonUtil -import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.apache.commons.io.FileUtils import org.eclipse.jetty.servlet.FilterHolder import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.webapp.WebAppContext @@ -23,18 +22,18 @@ import java.io.File abstract class ApplicationBase( final override val applicationName: String, - val oauthConfig: String? = null, resourceBase: String = "simpleSession", val temperature: Double = 0.1, ) : ChatServer(resourceBase) { - class ApplicationSession( - val parent: ApplicationBase, + inner class ApplicationSession( sessionId: String, + userId: String?, ) : SessionBase( sessionId = sessionId, - sessionDataStorage = parent.sessionDataStorage + dataStorage = dataStorage, + userId = userId, ) { private val threads = mutableMapOf() @@ -44,7 +43,7 @@ abstract class ApplicationBase( val operationID = randomID() val sessionDiv = newSessionDiv(operationID, spinner, true) threads[operationID] = Thread.currentThread() - parent.processMessage(sessionId, userMessage, this, sessionDiv, socket) + processMessage(sessionId, userMessage, this, sessionDiv, socket) } override fun onCmd(id: String, code: String, socket: ChatSocket) { @@ -60,8 +59,8 @@ abstract class ApplicationBase( fun htmlTools(divID: String) = HtmlTools(this, divID) } - override fun newSession(sessionId: String): SessionInterface { - return ApplicationSession(this, sessionId) + override fun newSession(userId: String?, sessionId: String): SessionInterface { + return ApplicationSession(sessionId, userId) } abstract fun processMessage( @@ -76,38 +75,33 @@ abstract class ApplicationBase( open fun initSettings(sessionId: String): T? = null - fun getSettings(sessionId: String): T? { + fun getSettings(sessionId: String, userId: String?): T? { @Suppress("UNCHECKED_CAST") - var settings: T? = sessionDataStorage.getSettings(sessionId, settingsClass as Class) + var settings: T? = dataStorage.getJson(userId, sessionId, settingsClass as Class, "settings.json") if (null == settings) { settings = initSettings(sessionId) if (null != settings) { - sessionDataStorage.updateSettings(sessionId, settings) + dataStorage.setJson(userId, sessionId, settings, "settings.json") } } return settings } - fun updateSettings(sessionId: String, settings: T) { - sessionDataStorage.updateSettings(sessionId, settings) - } - - final override val sessionDataStorage = SessionDataStorage(File(File(".skyenet"), applicationName)) - + 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 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", SessionServlet(this.dataStorage, path)) override fun configure(webAppContext: WebAppContext, path: String, baseUrl: String) { super.configure(webAppContext, path, baseUrl) - if (null != oauthConfig) AuthenticatedWebsite( - "$baseUrl/oauth2callback", - this@ApplicationBase.applicationName - ) { FileUtils.openInputStream(File(oauthConfig)) } - .configure(webAppContext) - webAppContext.addFilter( FilterHolder { request, response, chain -> - val user = AuthenticatedWebsite.getUser(request as HttpServletRequest) - val canRead = isAuthorized( + val user = authenticationManager.getUser(getCookie(request as HttpServletRequest, COOKIE_NAME)) + val canRead = authorizationManager.isAuthorized( applicationClass = this@ApplicationBase.javaClass, user = user?.email, operationType = AuthorizationManager.OperationType.Read @@ -121,21 +115,14 @@ abstract class ApplicationBase( }, "/*", null ) - val fileZip = ServletHolder("fileZip", ZipServlet(sessionDataStorage)) - val fileIndex = ServletHolder("fileIndex", FileServlet(sessionDataStorage)) - val sessionsServlet = ServletHolder("sessionList", SessionServlet(this.sessionDataStorage, path)) - val sessionSettingsServlet = ServletHolder("settings", SessionSettingsServlet(this)) - webAppContext.addServlet(appInfo, "/appInfo") webAppContext.addServlet(userInfo, "/userInfo") webAppContext.addServlet(fileIndex, "/fileIndex/*") webAppContext.addServlet(fileZip, "/fileZip") - webAppContext.addServlet(sessionsServlet, "/sessions") + webAppContext.addServlet(sessionsServlet(path), "/sessions") webAppContext.addServlet(sessionSettingsServlet, "/settings") } - protected open val appInfo = ServletHolder("appInfo", AppInfoServlet()) - protected open val userInfo = ServletHolder("userInfo", UserInfoServlet()) companion object { val log = LoggerFactory.getLogger(ApplicationBase::class.java) @@ -155,6 +142,9 @@ abstract class ApplicationBase( filename.endsWith(".css") -> "text/css" else -> "text/plain" } + + fun getCookie(req: HttpServletRequest, name: String) = req.cookies?.find { it.name == name }?.value + } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationDirectory.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationDirectory.kt index f6888c73..52e2ce6f 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationDirectory.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/ApplicationDirectory.kt @@ -1,29 +1,23 @@ package com.simiacryptus.skyenet -import com.google.api.services.oauth2.model.Userinfo import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.servlet.* -import com.simiacryptus.skyenet.session.SessionDataStorage import com.simiacryptus.skyenet.chat.ChatServer -import com.simiacryptus.skyenet.util.AuthorizationManager +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.servlet.* import com.simiacryptus.skyenet.util.AwsUtil.decryptResource import jakarta.servlet.DispatcherType import jakarta.servlet.Servlet -import jakarta.servlet.http.HttpServlet -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.* import org.eclipse.jetty.server.handler.ContextHandlerCollection import org.eclipse.jetty.servlet.FilterHolder import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.util.resource.Resource import org.eclipse.jetty.webapp.WebAppContext import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer -import org.intellij.lang.annotations.Language +import org.slf4j.LoggerFactory import java.awt.Desktop import java.net.URI -import java.nio.file.NoSuchFileException import java.util.* import kotlin.system.exitProcess @@ -32,7 +26,10 @@ abstract class ApplicationDirectory( private val publicName: String = "localhost", private val port: Int = 8081, ) { - var domainName: String = "" + var domainName: String = "" // Resolved in _main + private set(value) { + field = value + } abstract val childWebApps: List data class ChildWebApp( @@ -43,38 +40,33 @@ abstract class ApplicationDirectory( private fun domainName(isServer: Boolean) = if (isServer) "https://$publicName" else "http://$localName:$port" - val welcomeResources = Resource.newResource(javaClass.classLoader.getResource("welcome")) - val userInfoServlet = UserInfoServlet() - val userSettingsServlet = UserSettingsServlet() - val usageServlet = UsageServlet() + open val welcomeResources = Resource.newResource(javaClass.classLoader.getResource("welcome")) + ?: throw IllegalStateException("No welcome resource") + open val userInfoServlet = UserInfoServlet() + open val userSettingsServlet = UserSettingsServlet() + open val usageServlet = UsageServlet() + open val proxyHttpServlet = ProxyHttpServlet() + open val welcomeServlet = WelcomeServlet(this) + open fun authenticatedWebsite(): AuthenticatedWebsite? = AuthenticatedWebsite( + redirectUri = "$domainName/oauth2callback", + applicationName = "Demo", + key = { decryptResource("client_secret_google_oauth.json.kms").byteInputStream() } + ) - protected fun _main(args: Array) { + protected open fun _main(args: Array) { try { - OutputInterceptor.setupInterceptor() - val isServer = args.contains("--server") - domainName = domainName(isServer) + init(args.contains("--server")) OpenAIClient.keyTxt = decryptResource("openai.key.kms", javaClass.classLoader) - - val authentication = AuthenticatedWebsite( - redirectUri = "$domainName/oauth2callback", - applicationName = "Demo", - key = { decryptResource("client_secret_google_oauth.json.kms").byteInputStream() } - ) - + ApplicationServices.isLocked = true + val welcomeContext = newWebAppContext("/", welcomeResources, welcomeServlet) val server = start( port, *(arrayOf( newWebAppContext("/userInfo", userInfoServlet), newWebAppContext("/userSettings", userSettingsServlet), newWebAppContext("/usage", usageServlet), - newWebAppContext("/proxy", ProxyHttpServlet()), - authentication.configure( - newWebAppContext( - "/", - welcomeResources, - WelcomeServlet() - ), false - ), + newWebAppContext("/proxy", proxyHttpServlet), + authenticatedWebsite()?.configure(welcomeContext, false) ?: welcomeContext, ) + childWebApps.map { newWebAppContext(it.path, it.server) }) @@ -95,97 +87,13 @@ abstract class ApplicationDirectory( } } - private inner class WelcomeServlet : HttpServlet() { - override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) { - val user = AuthenticatedWebsite.getUser(req!!) - val requestURI = req.requestURI ?: "/" - resp?.contentType = when (requestURI) { - "/" -> "text/html" - else -> ApplicationBase.getMimeType(requestURI) - } - when { - requestURI == "/" -> resp?.writer?.write(homepage(user).trimIndent()) - requestURI == "/index.html" -> resp?.writer?.write(homepage(user).trimIndent()) - requestURI.startsWith("/userInfo") -> userInfoServlet.doGet(req, resp!!) - requestURI.startsWith("/userSettings") -> userSettingsServlet.doGet(req, resp!!) - requestURI.startsWith("/usage") -> usageServlet.doGet(req, resp!!) - else -> try { - val inputStream = welcomeResources.addPath(requestURI)?.inputStream - inputStream?.copyTo(resp?.outputStream!!) - } catch (e: NoSuchFileException) { - resp?.sendError(404) - } - } - } - - override fun doPost(req: HttpServletRequest?, resp: HttpServletResponse?) { - val requestURI = req?.requestURI ?: "/" - when { - requestURI.startsWith("/userSettings") -> userSettingsServlet.doPost(req!!, resp!!) - else -> resp?.sendError(404) - } - } - } - - private fun homepage(user: Userinfo?): String { - @Language("HTML") - val html = """ - - - - SimiaCryptus Skyenet Apps - - - - - - -
-
- -
- Login -
- - - ${ - childWebApps.joinToString("\n") { app -> - val canRun = AuthorizationManager.isAuthorized( - applicationClass = app.server.javaClass, - user = user?.email, - operationType = AuthorizationManager.OperationType.Write - ) - val canRead = AuthorizationManager.isAuthorized( - applicationClass = app.server.javaClass, - user = user?.email, - operationType = AuthorizationManager.OperationType.Read - ) - if (!canRead) return@joinToString "" - val newSessionLink = if(canRun) """New""" else "" - """ - - - - - - """.trimIndent() - } - } -
- ${app.server.applicationName} - - $newSessionLink - - List -
- - - - """ - return html + open fun init(isServer: Boolean): ApplicationDirectory { + OutputInterceptor.setupInterceptor() + domainName = domainName(isServer) + return this } - private fun start( + protected open fun start( port: Int, vararg webAppContexts: WebAppContext ): Server { @@ -195,19 +103,33 @@ abstract class ApplicationDirectory( it }.toTypedArray() val server = Server(port) + val serverConnector = ServerConnector(server, httpConnectionFactory()) + serverConnector.port = port + server.connectors = arrayOf(serverConnector) server.handler = contexts server.start() + if (!server.isStarted) throw IllegalStateException("Server failed to start") return server } - private fun newWebAppContext(path: String, server: ChatServer): WebAppContext { - val webAppContext = - newWebAppContext(path, server.baseResource ?: throw IllegalStateException("No base resource")) + protected open fun httpConnectionFactory(): HttpConnectionFactory { + val httpConfig = HttpConfiguration() + httpConfig.addCustomizer(ForwardedRequestCustomizer()) + return HttpConnectionFactory(httpConfig) + } + + protected open fun newWebAppContext(path: String, server: ChatServer): WebAppContext { + val baseResource = server.baseResource ?: throw IllegalStateException("No base resource") + val webAppContext = newWebAppContext(path, baseResource) server.configure(webAppContext, path = path, baseUrl = "$domainName/$path") return webAppContext } - private fun newWebAppContext(path: String, baseResource: Resource, indexServlet: Servlet? = null): WebAppContext { + protected open fun newWebAppContext( + path: String, + baseResource: Resource, + indexServlet: Servlet? = null + ): WebAppContext { val context = WebAppContext() JettyWebSocketServletContainerInitializer.configure(context, null) context.baseResource = baseResource @@ -220,7 +142,7 @@ abstract class ApplicationDirectory( return context } - private fun newWebAppContext(path: String, servlet: Servlet): WebAppContext { + protected open fun newWebAppContext(path: String, servlet: Servlet): WebAppContext { val context = WebAppContext() JettyWebSocketServletContainerInitializer.configure(context, null) context.contextPath = path @@ -228,4 +150,10 @@ abstract class ApplicationDirectory( context.addServlet(ServletHolder("index", servlet), "/") return context } + + + companion object { + val log = LoggerFactory.getLogger(ApplicationDirectory::class.java) + } + } \ No newline at end of file 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 09011418..771c92bf 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatServer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatServer.kt @@ -1,7 +1,9 @@ package com.simiacryptus.skyenet.chat +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager import com.simiacryptus.skyenet.servlet.NewSessionServlet -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.DataStorage import com.simiacryptus.skyenet.session.SessionInterface import com.simiacryptus.util.JsonUtil import jakarta.servlet.http.HttpServlet @@ -17,7 +19,7 @@ import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory abstract class ChatServer(val resourceBase: String) { abstract val applicationName: String - open val sessionDataStorage: SessionDataStorage? = null + open val dataStorage: DataStorage? = null val stateCache: MutableMap = mutableMapOf() inner class WebSocketHandler : JettyWebSocketServlet() { @@ -33,10 +35,13 @@ abstract class ChatServer(val resourceBase: String) { if (stateCache.containsKey(sessionId)) { sessionState = stateCache[sessionId]!! } else { - sessionState = newSession(sessionId) + sessionState = newSession( + ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id, sessionId) stateCache[sessionId] = sessionState } - ChatSocket(sessionId, sessionState, authId, sessionDataStorage) + ChatSocket(sessionId, sessionState, authId, dataStorage) } } catch (e: Exception) { log.warn("Error configuring websocket", e) @@ -59,7 +64,7 @@ abstract class ChatServer(val resourceBase: String) { } } - abstract fun newSession(sessionId: String): SessionInterface + abstract fun newSession(userId: String?, sessionId: String): SessionInterface open val baseResource: Resource? get() = Resource.newResource(javaClass.classLoader.getResource(resourceBase)) protected val newSessionServlet by lazy { NewSessionServlet() } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt index fad71493..fde4a853 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSession.kt @@ -14,7 +14,7 @@ open class ChatSession( private var systemPrompt: String, val api: OpenAIClient, val temperature: Double = 0.3, -) : SessionBase(sessionId, parent.sessionDataStorage) { +) : SessionBase(sessionId, parent.dataStorage, userId = null) { init { if (visiblePrompt.isNotBlank()) { 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 00919c36..f6633a61 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocket.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/chat/ChatSocket.kt @@ -1,14 +1,12 @@ package com.simiacryptus.skyenet.chat import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.servlet.AuthenticatedWebsite -import com.simiacryptus.skyenet.util.UsageManager.incrementUsage -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.ApplicationServices.authorizationManager +import com.simiacryptus.skyenet.config.DataStorage import com.simiacryptus.skyenet.session.SessionInterface -import com.simiacryptus.skyenet.util.AuthorizationManager -import com.simiacryptus.skyenet.util.AuthorizationManager.OperationType -import com.simiacryptus.skyenet.util.AuthorizationManager.isAuthorized -import com.simiacryptus.skyenet.util.UserSettingsManager.getUserSettings +import com.simiacryptus.skyenet.config.AuthorizationManager.OperationType +import com.simiacryptus.skyenet.config.AuthorizationManager.OperationType.GlobalKey import org.eclipse.jetty.websocket.api.Session import org.eclipse.jetty.websocket.api.WebSocketAdapter import org.slf4j.event.Level @@ -17,25 +15,25 @@ class ChatSocket( val sessionId: String, private val sessionState: SessionInterface, private val authId: String?, - val sessionDataStorage: SessionDataStorage?, + val dataStorage: DataStorage?, ) : WebSocketAdapter() { - val user get() = if (authId == null) null else AuthenticatedWebsite.users[authId] + val user get() = if (authId == null) null else ApplicationServices.authenticationManager.getUser(authId) - val userApi: OpenAIClient? + private val userApi: OpenAIClient? get() { val user = user - val userSettings = if (user == null) null else getUserSettings(user.id) + 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( - sessionDataStorage?.getSessionDir(sessionId)?.resolve("openai.log")?.outputStream()?.buffered() + dataStorage?.getSessionDir(user?.id, sessionId)?.resolve("openai.log")?.outputStream()?.buffered() ).filterNotNull().toMutableList(), ) { override fun incrementTokens(model: Model?, tokens: Int) { - incrementUsage(sessionId, user?.id, model!!, tokens) + ApplicationServices.usageManager.incrementUsage(sessionId, user?.id, model!!, tokens) super.incrementTokens(model, tokens) } } @@ -48,16 +46,16 @@ class ChatSocket( val user = user val userApi = userApi if (userApi != null) return userApi - val canUseGlobalKey = isAuthorized(null, user?.email, OperationType.GlobalKey) + val canUseGlobalKey = authorizationManager.isAuthorized(null, user?.email, GlobalKey) if (!canUseGlobalKey) throw RuntimeException("No API key") return object : OpenAIClient( logLevel = Level.DEBUG, logStreams = mutableListOf( - sessionDataStorage?.getSessionDir(sessionId)?.resolve("openai.log")?.outputStream()?.buffered() + dataStorage?.getSessionDir(user?.id, sessionId)?.resolve("openai.log")?.outputStream()?.buffered() ).filterNotNull().toMutableList() ) { override fun incrementTokens(model: Model?, tokens: Int) { - if(null != model) incrementUsage(sessionId, user?.id, model, tokens) + if(null != model) ApplicationServices.usageManager.incrementUsage(sessionId, user?.id, model, tokens) super.incrementTokens(model, tokens) } } 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 fff87b50..af90d843 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/AuthenticatedWebsite.kt @@ -7,6 +7,9 @@ import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport import com.google.api.client.json.gson.GsonFactory import com.google.api.services.oauth2.Oauth2 import com.google.api.services.oauth2.model.Userinfo +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager +import com.simiacryptus.skyenet.config.AuthenticationManager.Companion.COOKIE_NAME import jakarta.servlet.* import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServlet @@ -24,6 +27,7 @@ import java.nio.charset.StandardCharsets import java.util.* + open class AuthenticatedWebsite( val redirectUri: String, val applicationName: String, @@ -32,7 +36,12 @@ open class AuthenticatedWebsite( open fun newUserSession(userInfo: Userinfo, sessionId: String) { log.info("User $userInfo logged in with session $sessionId") - users[sessionId] = userInfo + ApplicationServices.authenticationManager.setUser(sessionId, AuthenticationManager.UserInfo( + id = userInfo.id, + email = userInfo.email, + name = userInfo.name, + picture = userInfo.picture + )) } open fun configure(context: WebAppContext, addFilter: Boolean = true): WebAppContext { @@ -88,8 +97,9 @@ open class AuthenticatedWebsite( override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { if (request is HttpServletRequest && response is HttpServletResponse) { if (isSecure(request)) { - val sessionIdCookie = request.cookies?.firstOrNull { it.name == "sessionId" } - if (sessionIdCookie == null || !users.containsKey(sessionIdCookie.value)) { + val sessionIdCookie = request.cookies?.firstOrNull { it.name == COOKIE_NAME } + val value = sessionIdCookie?.value + if (value == null || !ApplicationServices.authenticationManager.containsKey(value)) { response.sendRedirect("/googleLogin") return } @@ -112,7 +122,7 @@ open class AuthenticatedWebsite( val userInfo: Userinfo = oauth2.userinfo().get().execute() val sessionID = UUID.randomUUID().toString() newUserSession(userInfo, sessionID) - val sessionCookie = Cookie("sessionId", sessionID) + val sessionCookie = Cookie(COOKIE_NAME, sessionID) sessionCookie.path = "/" sessionCookie.isHttpOnly = false resp.addCookie(sessionCookie) @@ -127,17 +137,13 @@ open class AuthenticatedWebsite( companion object { private val log = org.slf4j.LoggerFactory.getLogger(AuthenticatedWebsite::class.java) - val users = HashMap() - fun getUser(req: HttpServletRequest): Userinfo? { - val sessionId = req.cookies?.find { it.name == "sessionId" }?.value - return if (null == sessionId) null else users[sessionId] + fun String.urlDecode(): String? = try { + URLDecoder.decode(this, StandardCharsets.UTF_8.toString()) + } catch (e: UnsupportedEncodingException) { + this } + } } -fun String.urlDecode(): String? = try { - URLDecoder.decode(this, StandardCharsets.UTF_8.toString()) -} catch (e: UnsupportedEncodingException) { - this -} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt index 1b9174dc..7b0da58a 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/FileServlet.kt @@ -1,13 +1,15 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.skyenet.ApplicationBase -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager +import com.simiacryptus.skyenet.config.DataStorage import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import java.io.File -class FileServlet(val sessionDataStorage: SessionDataStorage) : HttpServlet() { +class FileServlet(val dataStorage: DataStorage) : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { val path = req.pathInfo ?: "/" val pathSegments = path.split("/").filter { it.isNotBlank() } @@ -15,7 +17,9 @@ class FileServlet(val sessionDataStorage: SessionDataStorage) : HttpServlet() { if (it == "..") throw IllegalArgumentException("Invalid path") } val sessionID = pathSegments.first() - val sessionDir = sessionDataStorage.getSessionDir(sessionID) + val sessionDir = dataStorage.getSessionDir(ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id, sessionID) val filePath = pathSegments.drop(1).joinToString("/") val file = File(sessionDir, filePath) if (file.isFile) { 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 c1a30848..ed26808e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/NewSessionServlet.kt @@ -1,13 +1,13 @@ package com.simiacryptus.skyenet.servlet -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.DataStorage import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse class NewSessionServlet : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - val sessionId = SessionDataStorage.newID() + val sessionId = DataStorage.newGlobalID() resp.contentType = "text/plain" resp.status = HttpServletResponse.SC_OK resp.writer.write(sessionId) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt index 2d86bdbe..95d94db6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionServlet.kt @@ -1,18 +1,28 @@ package com.simiacryptus.skyenet.servlet -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager +import com.simiacryptus.skyenet.config.DataStorage import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -class SessionServlet(private val sessionDataStorage: SessionDataStorage, val prefix: String) : HttpServlet() { +class SessionServlet( + private val dataStorage: DataStorage, + private val prefix: String +) : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK - val sessions = sessionDataStorage.listSessions() + val sessions = dataStorage.listSessions(ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id) // onclick="window.location.href='#$session';window.location.reload();" val links = sessions.joinToString("
") { session -> - val sessionName = sessionDataStorage.getSessionName(session) + val sessionName = dataStorage.getSessionName( + ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id, session) """ |$sessionName |
""".trimMargin() 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 ccf386ef..cbe10da4 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/SessionSettingsServlet.kt @@ -1,6 +1,8 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.skyenet.ApplicationBase +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager import com.simiacryptus.util.JsonUtil import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -14,7 +16,9 @@ class SessionSettingsServlet( resp.status = HttpServletResponse.SC_OK val sessionId = req.getParameter("sessionId") if (null != sessionId) { - val settings = server.getSettings(sessionId) + val settings = server.getSettings(sessionId, ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id) val json = if(settings != null) JsonUtil.toJson(settings) else "" //language=HTML resp.writer.write( @@ -50,7 +54,9 @@ class SessionSettingsServlet( resp.writer.write("Session ID is required") } else { val settings = JsonUtil.fromJson(req.getParameter("settings"), server.settingsClass) - server.sessionDataStorage.updateSettings(sessionId, settings) + server.dataStorage.setJson(ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id, sessionId, settings, "settings.json") resp.sendRedirect("${req.contextPath}/#$sessionId") } } 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 82f456bd..f005b638 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UsageServlet.kt @@ -1,8 +1,9 @@ package com.simiacryptus.skyenet.servlet import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.util.UsageManager.getSessionUsageSummary -import com.simiacryptus.skyenet.util.UsageManager.getUserUsageSummary +import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager.Companion.COOKIE_NAME import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -14,13 +15,13 @@ class UsageServlet : HttpServlet() { val sessionId = req.getParameter("sessionId") if (null != sessionId) { - serve(resp, getSessionUsageSummary(sessionId)) + serve(resp, ApplicationServices.usageManager.getSessionUsageSummary(sessionId)) } else { - val userinfo = AuthenticatedWebsite.getUser(req) + val userinfo = ApplicationServices.authenticationManager.getUser(getCookie(req, COOKIE_NAME)) if (null == userinfo) { resp.status = HttpServletResponse.SC_BAD_REQUEST } else { - val usage = getUserUsageSummary(userinfo.id) + val usage = ApplicationServices.usageManager.getUserUsageSummary(userinfo.id) serve(resp, usage) } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt index 5cc5dbe6..d4a23ab0 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserInfoServlet.kt @@ -1,5 +1,8 @@ package com.simiacryptus.skyenet.servlet +import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager.Companion.COOKIE_NAME import com.simiacryptus.util.JsonUtil import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -9,7 +12,7 @@ class UserInfoServlet : HttpServlet() { public override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { resp.contentType = "text/json" resp.status = HttpServletResponse.SC_OK - val userinfo = AuthenticatedWebsite.getUser(req) + val userinfo = ApplicationServices.authenticationManager.getUser(getCookie(req, COOKIE_NAME)) if (null == userinfo) { resp.writer.write("{}") } else { 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 222555f5..3e6088b4 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/UserSettingsServlet.kt @@ -1,8 +1,9 @@ package com.simiacryptus.skyenet.servlet -import com.simiacryptus.skyenet.util.UserSettingsManager.UserSettings -import com.simiacryptus.skyenet.util.UserSettingsManager.getUserSettings -import com.simiacryptus.skyenet.util.UserSettingsManager.updateUserSettings +import com.simiacryptus.skyenet.ApplicationBase.Companion.getCookie +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager.Companion.COOKIE_NAME +import com.simiacryptus.skyenet.config.UserSettingsManager.UserSettings import com.simiacryptus.util.JsonUtil import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -12,11 +13,11 @@ class UserSettingsServlet : HttpServlet() { public override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { resp.contentType = "text/html" resp.status = HttpServletResponse.SC_OK - val userinfo = AuthenticatedWebsite.getUser(req) + val userinfo = ApplicationServices.authenticationManager.getUser(getCookie(req, COOKIE_NAME)) if (null == userinfo) { resp.status = HttpServletResponse.SC_BAD_REQUEST } else { - val settings = getUserSettings(userinfo.id) + val settings = ApplicationServices.userSettingsManager.getUserSettings(userinfo.id) val json = JsonUtil.toJson(settings) //language=HTML resp.writer.write( @@ -40,12 +41,12 @@ class UserSettingsServlet : HttpServlet() { } public override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { - val userinfo = AuthenticatedWebsite.getUser(req) + val userinfo = ApplicationServices.authenticationManager.getUser(getCookie(req, COOKIE_NAME)) if (null == userinfo) { resp.status = HttpServletResponse.SC_BAD_REQUEST } else { val settings = JsonUtil.fromJson(req.getParameter("settings"), UserSettings::class.java) - updateUserSettings(userinfo.id, settings) + ApplicationServices.userSettingsManager.updateUserSettings(userinfo.id, 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 new file mode 100644 index 00000000..6e79b735 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/WelcomeServlet.kt @@ -0,0 +1,117 @@ +package com.simiacryptus.skyenet.servlet + +import com.simiacryptus.skyenet.ApplicationBase +import com.simiacryptus.skyenet.ApplicationDirectory +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager +import com.simiacryptus.skyenet.config.AuthorizationManager +import com.simiacryptus.skyenet.config.DataStorage +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.intellij.lang.annotations.Language +import java.nio.file.NoSuchFileException + +open class WelcomeServlet(private val parent : ApplicationDirectory) : HttpServlet() { + override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) { + val user = ApplicationServices.authenticationManager.getUser( + ApplicationBase.getCookie( + req!!, + AuthenticationManager.COOKIE_NAME + ) + ) + val requestURI = req.requestURI ?: "/" + resp?.contentType = when (requestURI) { + "/" -> "text/html" + else -> ApplicationBase.getMimeType(requestURI) + } + when { + requestURI == "/" -> resp?.writer?.write(homepage(user).trimIndent()) + requestURI == "/index.html" -> resp?.writer?.write(homepage(user).trimIndent()) + requestURI.startsWith("/userInfo") -> { + parent.userInfoServlet.doGet(req, resp!!) + } + + requestURI.startsWith("/userSettings") -> parent.userSettingsServlet.doGet(req, resp!!) + requestURI.startsWith("/usage") -> parent.usageServlet.doGet(req, resp!!) + else -> try { + val inputStream = parent.welcomeResources.addPath(requestURI)?.inputStream + inputStream?.copyTo(resp?.outputStream!!) + } catch (e: NoSuchFileException) { + resp?.sendError(404) + } + } + } + + override fun doPost(req: HttpServletRequest?, resp: HttpServletResponse?) { + val requestURI = req?.requestURI ?: "/" + when { + requestURI.startsWith("/userSettings") -> parent.userSettingsServlet.doPost(req!!, resp!!) + else -> resp?.sendError(404) + } + } + + protected open fun homepage(user: AuthenticationManager.UserInfo?): String { + @Language("HTML") + val html = """ + + + + SimiaCryptus Skyenet Apps + + + + + + +
+
+ +
+ Login +
+ + + ${ + parent.childWebApps.joinToString("\n") { app -> + val canRun = ApplicationServices.authorizationManager.isAuthorized( + applicationClass = app.server.javaClass, + user = user?.email, + operationType = AuthorizationManager.OperationType.Write + ) + val canRead = ApplicationServices.authorizationManager.isAuthorized( + applicationClass = app.server.javaClass, + user = user?.email, + operationType = AuthorizationManager.OperationType.Read + ) + if (!canRead) return@joinToString "" + val newGlobalSessionLink = + if (canRun) """New Shared Session""" else "" + val newUserSessionLink = + if (canRun) """New Private Session""" else "" + """ + + + + + + + """.trimIndent() + } + } +
+ ${app.server.applicationName} + + List Sessions + + $newGlobalSessionLink + + $newUserSessionLink +
+ + + + """ + return html + } +} \ No newline at end of file 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 22f02260..4127e942 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servlet/ZipServlet.kt @@ -1,6 +1,8 @@ package com.simiacryptus.skyenet.servlet -import com.simiacryptus.skyenet.session.SessionDataStorage +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager +import com.simiacryptus.skyenet.config.DataStorage import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -8,11 +10,15 @@ import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -class ZipServlet(val sessionDataStorage: SessionDataStorage) : HttpServlet() { +class ZipServlet(val dataStorage: DataStorage) : HttpServlet() { override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { val sessionID = req.getParameter("session") val path = req.parameterMap.get("path")?.find { it.isNotBlank() } ?: "/" - val sessionDir = sessionDataStorage.getSessionDir(sessionID) + val sessionDir = dataStorage.getSessionDir( + ApplicationServices.authenticationManager.getUser( + req.cookies?.find { it.name == AuthenticationManager.COOKIE_NAME }?.value + )?.id, sessionID + ) val file = File(sessionDir, path) val zipFile = File.createTempFile("skynet", ".zip") try { diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt index 9e8f23c2..a1a1b42a 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionBase.kt @@ -1,18 +1,21 @@ package com.simiacryptus.skyenet.session -import com.google.api.services.oauth2.model.Userinfo import com.google.common.util.concurrent.MoreExecutors import com.simiacryptus.skyenet.chat.ChatServer import com.simiacryptus.skyenet.chat.ChatSocket -import com.simiacryptus.skyenet.util.AuthorizationManager -import com.simiacryptus.skyenet.util.AuthorizationManager.isAuthorized +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthorizationManager +import com.simiacryptus.skyenet.config.DataStorage import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger abstract class SessionBase( val sessionId: String, - private val sessionDataStorage: SessionDataStorage?, - private val messageStates: LinkedHashMap = sessionDataStorage?.loadMessages(sessionId) ?: LinkedHashMap(), + private val dataStorage: DataStorage?, + val userId: String? = null, + private val messageStates: LinkedHashMap = dataStorage?.getMessages( + userId, sessionId + ) ?: LinkedHashMap(), ) : SessionInterface { private val sockets: MutableSet = mutableSetOf() @@ -76,7 +79,7 @@ abstract class SessionBase( private fun setMessage(key: String, value: String): Int { if (messageStates.containsKey(key) && messageStates[key] == value) return -1 - sessionDataStorage?.updateMessage(sessionId, key, value) + dataStorage?.updateMessage(userId, sessionId, key, value) messageStates.put(key, value) return messageVersions.computeIfAbsent(key) { AtomicInteger(0) }.incrementAndGet() } @@ -103,11 +106,12 @@ abstract class SessionBase( } } catch (e: Exception) { log.warn("$sessionId - Error processing message: $message", e) + send("""${randomID()},
${e.message}
"""); } } } - open fun canWrite(user: String?) = isAuthorized( + open fun canWrite(user: String?) = ApplicationServices.authorizationManager.isAuthorized( applicationClass = this::class.java, user = user, operationType = AuthorizationManager.OperationType.Write diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDataStorage.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDataStorage.kt deleted file mode 100644 index ae1186dc..00000000 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/session/SessionDataStorage.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.simiacryptus.skyenet.session - -import com.simiacryptus.util.JsonUtil -import java.io.File -import java.util.* -import kotlin.collections.LinkedHashMap - -open class SessionDataStorage( - val dataDir: File = File("sessionData") -) { - - open fun updateMessage(sessionId: String, messageId: String, value: String) { - validateSessionId(sessionId) - val file = File(getMessageDir(sessionId), "$messageId.json") - log.debug("Updating message for $sessionId / $messageId: ${file.absolutePath}") - file.parentFile.mkdirs() - JsonUtil.objectMapper().writeValue(file, value) - } - - open fun loadMessages(sessionId: String): LinkedHashMap { - validateSessionId(sessionId) - val messageDir = getMessageDir(sessionId) - val messages = LinkedHashMap() - log.debug("Loading messages for $sessionId: ${messageDir.absolutePath}") - messageDir.listFiles()?.sortedBy { it.lastModified() }?.forEach { file -> - val message = JsonUtil.objectMapper().readValue(file, String::class.java) - messages[file.nameWithoutExtension] = message - } - log.debug("Loaded ${messages.size} messages for $sessionId") - return messages - } - - protected open fun getMessageDir(sessionId: String): File { - validateSessionId(sessionId) - val sessionDir = getInstanceDir(sessionId) - val messageDir = File(sessionDir, "messages") - log.debug("Message Dir for $sessionId: ${messageDir.absolutePath}") - return messageDir - } - - open fun listSessions(): List { - val files = dataDir.listFiles()?.flatMap { it.listFiles()?.toList() ?: listOf() }?.filter { sessionDir -> - val operationDir = File(sessionDir, "messages") - if (!operationDir.exists()) false else { - val listFiles = operationDir.listFiles().filter { it.isFile && !it.name.startsWith("aaa") } - (listFiles?.size ?: 0) > 0 - } - } - log.debug("Sessions: {}", files?.map { it.parentFile.name + "-" + it.name }) - return files?.map { it.parentFile.name + "-" + it.name } ?: listOf() - } - - open fun getSessionName(sessionId: String): String { - val userMessages = getUserMessages(sessionId) - if (userMessages.size > 0) { - val first = userMessages.first() - log.debug("Session $sessionId: ${first}") - return first - } else { - log.debug("Session $sessionId: No messages") - return sessionId - } - } - - @Suppress("MemberVisibilityCanBePrivate", "Unused") - fun getUserMessages(sessionId: String): List { - validateSessionId(sessionId) - val userMessages = getMessageDir(sessionId).listFiles()?.filter { file -> - file.isFile - }?.sortedBy { file -> - file.lastModified() - //JsonUtil.objectMapper().readValue(file, OperationStatus::class.java).created - }?.map { messageFile -> - val fileText = messageFile.readText() - val split = fileText.split("

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

") - if (stringList.isEmpty()) { - log.debug("Session $sessionId: No messages") - "" - } else { - stringList.first() - } - } - }?.filter { it.isNotEmpty() } ?: listOf() - return userMessages - } - - protected open fun getInstanceDir(sessionId: String): File { - validateSessionId(sessionId) - val sessionDir = File(getDateDir(sessionId), getInstanceId(sessionId)) - log.debug("Instance Dir for $sessionId: ${sessionDir.absolutePath}") - return sessionDir - } - - open fun getSessionDir(sessionId: String) = getInstanceDir(sessionId) - - protected open fun getDateDir(sessionId: String): File { - validateSessionId(sessionId) - val sessionGroupDir = File(dataDir, getDate(sessionId)) - log.debug("Date Dir for $sessionId: ${sessionGroupDir.absolutePath}") - return sessionGroupDir - } - - protected open fun getDate(sessionId: String): String { - validateSessionId(sessionId) - return sessionId.split("-").firstOrNull() ?: sessionId - } - - protected open fun getInstanceId(sessionId: String): String { - validateSessionId(sessionId) - return stripPrefix(stripPrefix(sessionId, getDate(sessionId)), "-") - } - - fun getSettings(sessionId: String, clazz: Class): T? { - validateSessionId(sessionId) - val settingsFile = File(getSessionDir(sessionId), "settings.json") - return if (!settingsFile.exists()) null else { - JsonUtil.objectMapper().readValue(settingsFile, clazz) as T - } - } - - fun updateSettings(sessionId: String, settings: T): T { - validateSessionId(sessionId) - val settingsFile = File(getSessionDir(sessionId), "settings.json") - settingsFile.parentFile.mkdirs() - JsonUtil.objectMapper().writeValue(settingsFile, settings) - return settings - } - - - companion object { - fun stripPrefix(text: String, prefix: String): String { - val startsWith = text.startsWith(prefix) - return if (startsWith) { - text.substring(prefix.length) - } else { - text - } - } - - fun newID(): String { - val uuid = UUID.randomUUID().toString().split("-").first() - val yyyyMMdd = java.time.LocalDate.now().toString().replace("-", "") - log.debug("New ID: $yyyyMMdd-$uuid") - return "$yyyyMMdd-$uuid" - } - - fun validateSessionId(sessionId: String) { - if (!sessionId.matches("""\d{8}-\w{8}""".toRegex())) { - throw IllegalArgumentException("Invalid session ID: $sessionId") - } - } - - private val log = org.slf4j.LoggerFactory.getLogger(SessionDataStorage::class.java) - - } -} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt index f6ba6c60..def5b44b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/CodingActorTestApp.kt @@ -3,10 +3,9 @@ package com.simiacryptus.skyenet.test import com.simiacryptus.skyenet.ApplicationBase import com.simiacryptus.skyenet.actors.CodingActor import com.simiacryptus.skyenet.chat.ChatSocket +import com.simiacryptus.skyenet.config.ApplicationServices import com.simiacryptus.skyenet.session.* -import com.simiacryptus.skyenet.util.AuthorizationManager -import com.simiacryptus.skyenet.util.AuthorizationManager.isAuthorized -import com.simiacryptus.skyenet.util.HtmlTools +import com.simiacryptus.skyenet.config.AuthorizationManager import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.util.* @@ -18,7 +17,6 @@ open class CodingActorTestApp( oauthConfig: String? = null, ) : ApplicationBase( applicationName = applicationName, - oauthConfig = oauthConfig, temperature = temperature, ) { @@ -31,7 +29,7 @@ open class CodingActorTestApp( ) { sessionDiv.append("""
${renderMarkdown(userMessage)}
""", true) val response = actor.answer(userMessage, api = socket.api) - val canPlay = isAuthorized( + val canPlay = ApplicationServices.authorizationManager.isAuthorized( this::class.java, socket.user?.email, AuthorizationManager.OperationType.Execute 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 f24ca302..0b253d64 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/ParsedActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/ParsedActorTestApp.kt @@ -15,7 +15,6 @@ open class ParsedActorTestApp( oauthConfig: String? = null, ) : ApplicationBase( applicationName = applicationName, - oauthConfig = oauthConfig, temperature = temperature, ) { override fun processMessage( 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 95477548..1bd7dfd2 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/test/SimpleActorTestApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/test/SimpleActorTestApp.kt @@ -14,7 +14,6 @@ open class SimpleActorTestApp( oauthConfig: String? = null, ) : ApplicationBase( applicationName = applicationName, - oauthConfig = oauthConfig, temperature = temperature, ) { @@ -31,7 +30,7 @@ open class SimpleActorTestApp( sessionDiv: SessionDiv, socket: ChatSocket ) { - val actor = getSettings(sessionId)?.actor ?: actor + 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) 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 559691b4..7d060e7b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/EmbeddingVisualizer.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/EmbeddingVisualizer.kt @@ -1,15 +1,17 @@ package com.simiacryptus.skyenet.util import com.simiacryptus.openai.OpenAIClient -import com.simiacryptus.skyenet.session.* +import com.simiacryptus.skyenet.config.DataStorage +import com.simiacryptus.skyenet.session.SessionBase import com.simiacryptus.util.JsonUtil class EmbeddingVisualizer( val api: OpenAIClient, - val sessionDataStorage: SessionDataStorage, + val dataStorage: DataStorage, val sessionID: String, val appPath: String, val host: String = "http://localhost:8081", + val session: SessionBase, ) { private fun toVectorMap(vararg words: String): Map> { @@ -39,8 +41,8 @@ class EmbeddingVisualizer( val vectorFileName = "vectors.tsv" val metadataFileName = "metadata.tsv" val configFileName = "projector-config.json" - sessionDataStorage.getSessionDir(sessionID).resolve(vectorFileName).writeText(vectorTsv) - sessionDataStorage.getSessionDir(sessionID).resolve(metadataFileName).writeText(metadataTsv) + dataStorage.getSessionDir(session.userId, sessionID).resolve(vectorFileName).writeText(vectorTsv) + dataStorage.getSessionDir(session.userId, sessionID).resolve(metadataFileName).writeText(metadataTsv) // projector-config.json val projectorConfig = JsonUtil.toJson( mapOf( @@ -54,7 +56,7 @@ class EmbeddingVisualizer( ) ) ) - sessionDataStorage.getSessionDir(sessionID).resolve(configFileName).writeText(projectorConfig) + dataStorage.getSessionDir(session.userId, sessionID).resolve(configFileName).writeText(projectorConfig) return """ Projector Config Vectors diff --git a/webui/src/main/resources/simpleSession/chat.css b/webui/src/main/resources/simpleSession/chat.css index 0162e987..bb8708d2 100644 --- a/webui/src/main/resources/simpleSession/chat.css +++ b/webui/src/main/resources/simpleSession/chat.css @@ -276,3 +276,15 @@ pre { max-height: 0; transition: max-height 0.2s ease-out; } + +.error { + color: red; +} + +.verbose { + display: block; +} + +.verbose-hidden { + display: none; +} \ No newline at end of file diff --git a/webui/src/main/resources/simpleSession/index.html b/webui/src/main/resources/simpleSession/index.html index f96c6feb..0c64f797 100644 --- a/webui/src/main/resources/simpleSession/index.html +++ b/webui/src/main/resources/simpleSession/index.html @@ -19,6 +19,7 @@ Session Settings Files Usage + Hide Verbose
diff --git a/webui/src/main/resources/simpleSession/main.js b/webui/src/main/resources/simpleSession/main.js index 2eb82a5e..9e495c58 100644 --- a/webui/src/main/resources/simpleSession/main.js +++ b/webui/src/main/resources/simpleSession/main.js @@ -59,11 +59,31 @@ function onWebSocketText(event) { Prism.highlightAll(); } +function toggleVerbose() { + let verboseToggle = document.getElementById('verbose'); + if(verboseToggle.innerText === 'Hide Verbose') { + const elements = document.getElementsByClassName('verbose'); + for (let i = 0; i < elements.length; i++) { + elements[i].classList.add('verbose-hidden'); // Add the 'verbose-hidden' class to hide + } + verboseToggle.innerText = 'Show Verbose'; + } else if(verboseToggle.innerText === 'Show Verbose') { + const elements = document.getElementsByClassName('verbose'); + for (let i = 0; i < elements.length; i++) { + elements[i].classList.remove('verbose-hidden'); // Remove the 'verbose-hidden' class to show + } + verboseToggle.innerText = 'Hide Verbose'; + } else { + console.log("Error: Unknown state for verbose button"); + } +} + document.addEventListener('DOMContentLoaded', () => { document.getElementById('history').addEventListener('click', () => showModal('sessions')); document.getElementById('settings').addEventListener('click', () => showModal('settings')); document.getElementById('usage').addEventListener('click', () => showModal('usage')); + document.getElementById('verbose').addEventListener('click', () => toggleVerbose()); document.querySelector('.close').addEventListener('click', closeModal); window.addEventListener('click', (event) => { diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt index f99c9c79..29c27d68 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt @@ -3,6 +3,9 @@ package com.simiacryptus.skyenet import com.simiacryptus.skyenet.actors.CodingActor import com.simiacryptus.skyenet.actors.ParsedActor import com.simiacryptus.skyenet.actors.SimpleActor +import com.simiacryptus.skyenet.config.ApplicationServices +import com.simiacryptus.skyenet.config.AuthenticationManager +import com.simiacryptus.skyenet.config.AuthorizationManager import com.simiacryptus.skyenet.heart.GroovyInterpreter import com.simiacryptus.skyenet.heart.KotlinInterpreter import com.simiacryptus.skyenet.heart.ScalaLocalInterpreter @@ -33,6 +36,24 @@ object ActorTestAppServer : ApplicationDirectory(port = 8082) { @JvmStatic fun main(args: Array) { + val mockUser = AuthenticationManager.UserInfo( + "1", + "user@mock.test", + "Test User", + "" + ) + 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() + } + ApplicationServices.authorizationManager = object : AuthorizationManager() { + override fun isAuthorized( + applicationClass: Class<*>?, + user: String?, + operationType: OperationType + ): Boolean = true + } super._main(args) } }