From 5eabcfaac33e04f7d1a9e18ecd3f1ad40affcea5 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Thu, 29 Aug 2024 12:52:34 -0400 Subject: [PATCH] 1.0.88 (#93) * 1.0.88 * 1.0.68 * fixes * Update TaskPlanningTask.kt * wip --- README.md | 6 +- core/build.gradle.kts | 2 +- .../skyenet/core/actors/BaseActor.kt | 2 +- .../skyenet/core/actors/CodingActor.kt | 9 +- .../skyenet/core/actors/ImageActor.kt | 2 +- .../skyenet/core/actors/ParsedActor.kt | 5 +- .../skyenet/core/actors/SimpleActor.kt | 2 +- gradle.properties | 2 +- webui/build.gradle.kts | 2 +- .../simiacryptus/diff/FileValidationUtils.kt | 18 +- .../skyenet/apps/coding/CodingAgent.kt | 3 +- .../skyenet/apps/general/CmdPatchApp.kt | 161 +++++++ .../skyenet/apps/general/PatchApp.kt | 357 ++++++++++++++++ .../skyenet/apps/general/PlanAheadApp.kt | 66 +++ .../skyenet/apps/general/WebDevApp.kt | 118 +++--- .../skyenet/apps/plan/AbstractTask.kt | 84 ++++ .../skyenet/apps/plan/CommandAutoFixTask.kt | 121 ++++++ .../skyenet/apps/plan/DocumentationTask.kt | 89 ++++ .../skyenet/apps/plan/EditFileTask.kt | 146 +++++++ .../skyenet/apps/plan/InquiryTask.kt | 95 +++++ .../skyenet/apps/plan/NewFileTask.kt | 138 ++++++ .../skyenet/apps/plan/PlanCoordinator.kt | 392 ++++++++++++++++++ .../skyenet/apps/plan/PlanningTask.kt | 119 ++++++ .../skyenet/apps/plan/RunShellCommandTask.kt | 135 ++++++ .../skyenet/apps/plan/Settings.kt | 63 +++ .../skyenet/apps/plan/TaskType.kt | 11 + .../main/resources/application/appScript.js | 0 .../src/main/resources/application/index.html | 1 - webui/src/main/resources/application/main.js | 68 ++- webui/src/main/resources/application/tabs.js | 149 +++---- .../skyenet/webui/ActorTestAppServer.kt | 31 +- 31 files changed, 2229 insertions(+), 168 deletions(-) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/CmdPatchApp.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanAheadApp.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/CommandAutoFixTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/DocumentationTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/EditFileTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/InquiryTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/NewFileTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanCoordinator.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanningTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/Settings.kt create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/TaskType.kt delete mode 100644 webui/src/main/resources/application/appScript.js diff --git a/README.md b/README.md index c7f19b7e..5fcb1a5b 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.0.67 + 1.0.68 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.67' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.0.68' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.0.67") +implementation("com.simiacryptus:skyenet:1.0.68") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 938f00f7..ff7388f9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -33,7 +33,7 @@ val hsqldb_version = "2.7.2" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.67") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.68") implementation(group = "org.hsqldb", name = "hsqldb", version = hsqldb_version) implementation("org.apache.commons:commons-text:1.11.0") diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt index 9b6595a3..6c5918b0 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/BaseActor.kt @@ -10,7 +10,7 @@ import com.simiacryptus.jopenai.models.OpenAITextModel abstract class BaseActor( open val prompt: String, val name: String? = null, - val model: ChatModels, + val model: OpenAITextModel, val temperature: Double = 0.3, ) { abstract fun respond(input: I, api: API, vararg messages: ApiModel.ChatMessage): R diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt index 658b4120..d9220a4d 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt @@ -6,6 +6,7 @@ import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.describe.AbbrevWhitelistYamlDescriber import com.simiacryptus.jopenai.describe.TypeDescriber import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.skyenet.core.OutputInterceptor import com.simiacryptus.skyenet.interpreter.Interpreter @@ -22,7 +23,7 @@ open class CodingActor( ), name: String? = interpreterClass.simpleName, val details: String? = null, - model: ChatModels, + model: OpenAITextModel, val fallbackModel: ChatModels = ChatModels.GPT4o, temperature: Double = 0.1, val runtimeSymbols: Map = mapOf() @@ -237,7 +238,7 @@ open class CodingActor( override val code: String = givenCode ?: implementation.first private fun implement( - model: ChatModels, + model: OpenAITextModel, ): Pair { val request = ChatRequest(messages = ArrayList(this.messages.toList())) for (codingAttempt in 0..input.fixRetries) { @@ -312,7 +313,7 @@ open class CodingActor( previousCode: String, error: Throwable, vararg promptMessages: ChatMessage, - model: ChatModels + model: OpenAITextModel ): String = chat( api = api, request = ChatRequest( @@ -344,7 +345,7 @@ open class CodingActor( model = model ) - private fun chat(api: OpenAIClient, request: ChatRequest, model: ChatModels) = + private fun chat(api: OpenAIClient, request: ChatRequest, model: OpenAITextModel) = api.chat(request.copy(model = model.modelName, temperature = temperature), model) .choices.first().message?.content.orEmpty().trim() diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt index 43292f41..16ae1262 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ImageActor.kt @@ -18,7 +18,7 @@ import javax.imageio.ImageIO open class ImageActor( prompt: String = "Transform the user request into an image generation prompt that the user will like", name: String? = null, - textModel: ChatModels, + textModel: OpenAITextModel, val imageModel: ImageModels = ImageModels.DallE2, temperature: Double = 0.3, val width: Int = 1024, diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt index 1eef612e..a86699a4 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/ParsedActor.kt @@ -6,6 +6,7 @@ import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.describe.AbbrevWhitelistYamlDescriber import com.simiacryptus.jopenai.describe.TypeDescriber import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.jopenai.util.JsonUtil import java.util.function.Function @@ -15,9 +16,9 @@ open class ParsedActor( val exampleInstance: T? = resultClass?.getConstructor()?.newInstance(), prompt: String = "", name: String? = resultClass?.simpleName, - model: ChatModels = ChatModels.GPT4o, + model: OpenAITextModel = ChatModels.GPT4o, temperature: Double = 0.3, - val parsingModel: ChatModels = ChatModels.GPT35Turbo, + val parsingModel: OpenAITextModel = ChatModels.GPT35Turbo, val deserializerRetries: Int = 2, open val describer: TypeDescriber = object : AbbrevWhitelistYamlDescriber( "com.simiacryptus", "com.github.simiacryptus" diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt index d0344fe2..ed81625d 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/SimpleActor.kt @@ -9,7 +9,7 @@ import com.simiacryptus.jopenai.util.ClientUtil.toContentList open class SimpleActor( prompt: String, name: String? = null, - model: ChatModels, + model: OpenAITextModel, temperature: Double = 0.3, ) : BaseActor, String>( prompt = prompt, diff --git a/gradle.properties b/gradle.properties index 6f2a4b62..18a7de35 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Gradle Releases -> https://github.com/gradle/gradle/releases libraryGroup = com.simiacryptus.skyenet -libraryVersion = 1.0.87 +libraryVersion = 1.0.88 gradleVersion = 7.6.1 kotlin.daemon.jvmargs=-Xmx2g diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index ddcbdd04..fe451fd9 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -35,7 +35,7 @@ val jetty_version = "11.0.18" val jackson_version = "2.17.0" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.67") { + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.68") { exclude(group = "org.slf4j", module = "slf4j-api") } diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/FileValidationUtils.kt b/webui/src/main/kotlin/com/simiacryptus/diff/FileValidationUtils.kt index 3cea4cae..de21ad9e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/diff/FileValidationUtils.kt +++ b/webui/src/main/kotlin/com/simiacryptus/diff/FileValidationUtils.kt @@ -89,7 +89,7 @@ class FileValidationUtils { file.name.startsWith(".") -> false file.length() > (256 * 1024) -> false isGitignore(file.toPath()) -> false - file.extension?.lowercase(Locale.getDefault()) in setOf( + file.extension.lowercase(Locale.getDefault()) in setOf( "jar", "zip", "class", @@ -104,6 +104,20 @@ class FileValidationUtils { } } + fun expandFileList(vararg data: File): Array { + return data.flatMap { + (when { + it.name.startsWith(".") -> arrayOf() + isGitignore(it.toPath()) -> arrayOf() + it.length() > 1e6 -> arrayOf() + it.extension.lowercase(Locale.getDefault()) in + setOf("jar", "zip", "class", "png", "jpg", "jpeg", "gif", "ico") -> arrayOf() + it.isDirectory -> expandFileList(*it.listFiles() ?: arrayOf()) + else -> arrayOf(it) + }).toList() + }.toTypedArray() + } + fun isGitignore(path: Path): Boolean { var currentDir = path.toFile().parentFile currentDir ?: return false @@ -112,7 +126,7 @@ class FileValidationUtils { if (it.exists()) { val gitignore = it.readText() if (gitignore.split("\n").any { line -> - val pattern = line.trim().trimEnd('/').replace(".", "\\.").replace("*", ".*") + val pattern = line.trim().trimStart('/').trimEnd('/').replace(".", "\\.").replace("*", ".*") line.trim().isNotEmpty() && !line.startsWith("#") && path.fileName.toString().trimEnd('/').matches(Regex(pattern)) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt index c2b7f463..143fc8cf 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt @@ -4,6 +4,7 @@ import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.proxy.ValidatedObject import com.simiacryptus.skyenet.Retryable import com.simiacryptus.skyenet.core.actors.ActorSystem @@ -34,7 +35,7 @@ open class CodingAgent( val symbols: Map, temperature: Double = 0.1, val details: String? = null, - val model: ChatModels, + val model: OpenAITextModel, private val mainTask: SessionTask, val actorMap: Map = mapOf( ActorTypes.CodingActor to CodingActor( diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/CmdPatchApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/CmdPatchApp.kt new file mode 100644 index 00000000..aabda18f --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/CmdPatchApp.kt @@ -0,0 +1,161 @@ +package com.simiacryptus.skyenet.apps.general + + +import com.simiacryptus.diff.FileValidationUtils +import com.simiacryptus.jopenai.OpenAIClient +import com.simiacryptus.jopenai.models.OpenAITextModel +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.set +import com.simiacryptus.skyenet.webui.session.SessionTask +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Path +import java.util.concurrent.TimeUnit + +class CmdPatchApp( + root: Path, + session: Session, + settings: Settings, + api: OpenAIClient, + val virtualFiles: Array?, + model: OpenAITextModel +) : PatchApp(root.toFile(), session, settings, api, model) { + companion object { + private val log = LoggerFactory.getLogger(CmdPatchApp::class.java) + + val String.htmlEscape: String + get() = this.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + + fun truncate(output: String, kb: Int = 32): String { + var returnVal = output + if (returnVal.length > 1024 * 2 * kb) { + returnVal = returnVal.substring(0, 1024 * kb) + + "\n\n... Output truncated ...\n\n" + + returnVal.substring(returnVal.length - 1024 * kb) + } + return returnVal + } + + + } + + private fun getFiles( + virtualFiles: Array? + ): MutableSet { + val codeFiles = mutableSetOf() // Set to avoid duplicates + virtualFiles?.forEach { file -> + if (file.isDirectory) { + if (file.name.startsWith(".")) return@forEach + if (FileValidationUtils.isGitignore(file.toPath())) return@forEach + codeFiles.addAll(getFiles(file.listFiles())) + } else { + codeFiles.add((file.toPath())) + } + } + return codeFiles + } + + override fun codeFiles() = getFiles(virtualFiles) + .filter { it.toFile().length() < 1024 * 1024 / 2 } // Limit to 0.5MB + .map { root.toPath().relativize(it) ?: it }.toSet() + + override fun codeSummary(paths: List): String = paths + .filter { + val file = settings.workingDirectory?.resolve(it.toFile()) + file?.exists() == true && !file.isDirectory && file.length() < (256 * 1024) + } + .joinToString("\n\n") { path -> + try { + """ + |# ${path} + |${tripleTilde}${path.toString().split('.').lastOrNull()} + |${settings.workingDirectory?.resolve(path.toFile())?.readText(Charsets.UTF_8)} + |${tripleTilde} + """.trimMargin() + } catch (e: Exception) { + log.warn("Error reading file", e) + "Error reading file `${path}` - ${e.message}" + } + } + + override fun projectSummary(): String { + val codeFiles = codeFiles() + val str = codeFiles + .asSequence() + .filter { settings.workingDirectory?.toPath()?.resolve(it)?.toFile()?.exists() == true } + .distinct().sorted() + .joinToString("\n") { path -> + "* ${path} - ${ + settings.workingDirectory?.toPath()?.resolve(path)?.toFile()?.length() ?: "?" + } bytes".trim() + } + return str + } + + override fun output(task: SessionTask): OutputResult = run { + val command = + listOf(settings.executable.absolutePath) + settings.arguments.split(" ").filter(String::isNotBlank) + val processBuilder = ProcessBuilder(command).directory(settings.workingDirectory) + val buffer = StringBuilder() + val taskOutput = task.add("") + val process = processBuilder.start() + Thread { + var lastUpdate = 0L + process.errorStream.bufferedReader().use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + buffer.append(line).append("\n") + if (lastUpdate + TimeUnit.SECONDS.toMillis(15) < System.currentTimeMillis()) { + taskOutput?.set("
\n${truncate(buffer.toString()).htmlEscape}\n
") + task.append("", true) + lastUpdate = System.currentTimeMillis() + } + } + task.append("", true) + } + }.start() + process.inputStream.bufferedReader().use { reader -> + var line: String? + var lastUpdate = 0L + while (reader.readLine().also { line = it } != null) { + buffer.append(line).append("\n") + if (lastUpdate + TimeUnit.SECONDS.toMillis(15) < System.currentTimeMillis()) { + taskOutput?.set("
\n${outputString(buffer).htmlEscape}\n
") + task.append("", true) + lastUpdate = System.currentTimeMillis() + } + } + task.append("", true) + } + task.append("", false) + if (!process.waitFor(5, TimeUnit.MINUTES)) { + process.destroy() + throw RuntimeException("Process timed out") + } + val exitCode = process.exitValue() + var output = outputString(buffer) + taskOutput?.clear() + OutputResult(exitCode, output) + } + + private fun outputString(buffer: StringBuilder): String { + var output = buffer.toString() + output = output.replace(Regex("\\x1B\\[[0-?]*[ -/]*[@-~]"), "") // Remove terminal escape codes + output = truncate(output) + return output + } + + override fun searchFiles(searchStrings: List): Set { + return searchStrings.flatMap { searchString -> + FileValidationUtils.filteredWalk(settings.workingDirectory!!) { !FileValidationUtils.isGitignore(it.toPath()) } + .filter { FileValidationUtils.isLLMIncludable(it) } + .filter { it.readText().contains(searchString, ignoreCase = true) } + .map { it.toPath() } + .toList() + }.toSet() + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt new file mode 100644 index 00000000..4e44da9a --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt @@ -0,0 +1,357 @@ +package com.simiacryptus.skyenet.apps.general + +import com.simiacryptus.diff.FileValidationUtils +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.jopenai.OpenAIClient +import com.simiacryptus.jopenai.describe.Description +import com.simiacryptus.jopenai.models.OpenAITextModel +import com.simiacryptus.jopenai.util.JsonUtil +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.Retryable +import com.simiacryptus.skyenet.core.actors.ParsedActor +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.application.ApplicationServer +import com.simiacryptus.skyenet.webui.application.ApplicationSocketManager +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.session.SocketManager +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Path + +abstract class PatchApp( + override val root: File, + val session: Session, + val settings: Settings, + val api: OpenAIClient, + val model: OpenAITextModel +) : ApplicationServer( + applicationName = "Magic Code Fixer", + path = "/fixCmd", + showMenubar = false, +) { + companion object { + private val log = LoggerFactory.getLogger(PatchApp::class.java) + const val tripleTilde = "`" + "``" // This is a workaround for the markdown parser when editing this file + } + + data class OutputResult(val exitCode: Int, val output: String) + + abstract fun codeFiles(): Set + abstract fun codeSummary(paths: List): String + abstract fun output(task: SessionTask): OutputResult + abstract fun searchFiles(searchStrings: List): Set + override val singleInput = true + override val stickyInput = false + override fun newSession(user: User?, session: Session ): SocketManager { + val socketManager = super.newSession(user, session) + val ui = (socketManager as ApplicationSocketManager).applicationInterface + val task = ui.newTask() + Retryable( + ui = ui, + task = task, + process = { content -> + val newTask = ui.newTask(false) + newTask.add("Running Command") + Thread { + run(ui, newTask) + }.start() + newTask.placeholder + } + ) + return socketManager + } + + abstract fun projectSummary(): String + + private fun prunePaths(paths: List, maxSize: Int): List { + val sortedPaths = paths.sortedByDescending { it.toFile().length() } + var totalSize = 0 + val prunedPaths = mutableListOf() + for (path in sortedPaths) { + val fileSize = path.toFile().length().toInt() + if (totalSize + fileSize > maxSize) break + prunedPaths.add(path) + totalSize += fileSize + } + return prunedPaths + } + + data class ParsedErrors( + val errors: List? = null + ) + + data class ParsedError( + @Description("The error message") + val message: String? = null, + @Description("Files identified as needing modification and issue-related files") + val relatedFiles: List? = null, + @Description("Files identified as needing modification and issue-related files") + val fixFiles: List? = null, + @Description("Search strings to find relevant files") + val searchStrings: List? = null + ) + + data class Settings( + var executable: File, + var arguments: String = "", + var workingDirectory: File? = null, + var exitCodeOption: String = "nonzero", + var additionalInstructions: String = "", + val autoFix: Boolean, + ) + + fun run( + ui: ApplicationInterface, + task: SessionTask, + ): OutputResult { + val output = output(task) + if (output.exitCode == 0 && settings.exitCodeOption == "nonzero") { + task.complete( + """ + |
+ |
Command executed successfully
+ |${MarkdownUtil.renderMarkdown("${tripleTilde}\n${output.output}\n${tripleTilde}")} + |
+ |""".trimMargin() + ) + return output + } + if (settings.exitCodeOption == "zero" && output.exitCode != 0) { + task.complete( + """ + |
+ |
Command failed
+ |${MarkdownUtil.renderMarkdown("${tripleTilde}\n${output.output}\n${tripleTilde}")} + |
+ |""".trimMargin() + ) + return output + } + try { + task.add( + """ + |
+ |
Command exit code: ${output.exitCode}
+ |${MarkdownUtil.renderMarkdown("${tripleTilde}\n${output.output}\n${tripleTilde}")} + |
+ """.trimMargin() + ) + fixAll(settings, output, task, ui, api) + } catch (e: Exception) { + task.error(ui, e) + } + return output + } + + private fun fixAll( + settings: Settings, + output: OutputResult, + task: SessionTask, + ui: ApplicationInterface, + api: OpenAIClient, + ) { + Retryable(ui, task) { content -> + fixAllInternal( + settings = settings, + output = output, + task = task, + ui = ui, + changed = mutableSetOf(), + api = api + ) + content.clear() + "" + } + } + + private fun fixAllInternal( + settings: Settings, + output: OutputResult, + task: SessionTask, + ui: ApplicationInterface, + changed: MutableSet, + api: OpenAIClient, + ) { + val plan = ParsedActor( + resultClass = ParsedErrors::class.java, + prompt = """ + |You are a helpful AI that helps people with coding. + | + |You will be answering questions about the following project: + | + |Project Root: ${settings.workingDirectory?.absolutePath ?: ""} + | + |Files: + |${projectSummary()} + | + |Given the response of a build/test process, identify one or more distinct errors. + |For each error: + | 1) predict the files that need to be fixed + | 2) predict related files that may be needed to debug the issue + | 3) specify a search string to find relevant files - be as specific as possible + |${if (settings.additionalInstructions.isNotBlank()) "Additional Instructions:\n ${settings.additionalInstructions}\n" else ""} + """.trimMargin(), + model = model + ).answer( + listOf( + """ + |The following command was run and produced an error: + | + |${tripleTilde} + |${output.output} + |${tripleTilde} + """.trimMargin() + ), api = api + ) + task.add( + AgentPatterns.displayMapInTabs( + mapOf( + "Text" to MarkdownUtil.renderMarkdown(plan.text, ui = ui), + "JSON" to MarkdownUtil.renderMarkdown( + "${tripleTilde}json\n${JsonUtil.toJson(plan.obj)}\n${tripleTilde}", + ui = ui + ), + ) + ) + ) + val progressHeader = task.header("Processing tasks") + plan.obj.errors?.forEach { error -> + task.header("Processing error: ${error.message}") + task.verbose(MarkdownUtil.renderMarkdown("```json\n${JsonUtil.toJson(error)}\n```", tabs = false, ui = ui)) + // Search for files using the provided search strings + val searchResults = error.searchStrings?.flatMap { searchString -> + FileValidationUtils.filteredWalk(settings.workingDirectory!!) { !FileValidationUtils.isGitignore(it.toPath()) } + .filter { FileValidationUtils.isLLMIncludable(it) } + .filter { it.readText().contains(searchString, ignoreCase = true) } + .map { it.toPath() } + .toList() + }?.toSet() ?: emptySet() + task.verbose( + MarkdownUtil.renderMarkdown( + """ + |Search results: + | + |${searchResults.joinToString("\n") { "* `$it`" }} + """.trimMargin(), tabs = false, ui = ui + ) + ) + Retryable(ui, task) { content -> + fix( + error, searchResults.toList().map { it.toFile().absolutePath }, + output, ui, content, settings.autoFix, changed, api + ) + content.toString() + } + } + progressHeader?.clear() + task.append("", false) + } + + private fun fix( + error: ParsedError, + additionalFiles: List? = null, + output: OutputResult, + ui: ApplicationInterface, + content: StringBuilder, + autoFix: Boolean, + changed: MutableSet, + api: OpenAIClient, + ) { + val paths = + ( + (error.fixFiles ?: emptyList()) + + (error.relatedFiles ?: emptyList()) + + (additionalFiles ?: emptyList()) + ).map { + try { + File(it).toPath() + } catch (e: Throwable) { + log.warn("Error: root=${root} ", e) + null + } + }.filterNotNull() + val prunedPaths = prunePaths(paths, 50 * 1024) + val summary = codeSummary(prunedPaths) + val response = SimpleActor( + prompt = """ + |You are a helpful AI that helps people with coding. + | + |You will be answering questions about the following code: + | + |$summary + | + | + |Response should use one or more code patches in diff format within ${tripleTilde}diff code blocks. + |Each diff should be preceded by a header that identifies the file being modified. + |The diff format should use + for line additions, - for line deletions. + |The diff should include 2 lines of context before and after every change. + | + |Example: + | + |Here are the patches: + | + |### src/utils/exampleUtils.js + |${tripleTilde}diff + | // Utility functions for example feature + | const b = 2; + | function exampleFunction() { + |- return b + 1; + |+ return b + 2; + | } + |${tripleTilde} + | + |### tests/exampleUtils.test.js + |${tripleTilde}diff + | // Unit tests for exampleUtils + | const assert = require('assert'); + | const { exampleFunction } = require('../src/utils/exampleUtils'); + | + | describe('exampleFunction', () => { + |- it('should return 3', () => { + |+ it('should return 4', () => { + | assert.equal(exampleFunction(), 3); + | }); + | }); + |${tripleTilde} + | + |If needed, new files can be created by using code blocks labeled with the filename in the same manner. + """.trimMargin(), + model = model + ).answer( + listOf( + """ + |The following command was run and produced an error: + | + |${tripleTilde} + |${output.output} + |${tripleTilde} + | + |Focus on and Fix the Error: + | ${error.message?.replace("\n", "\n ") ?: ""} + |${if (settings.additionalInstructions.isNotBlank()) "Additional Instructions:\n ${settings.additionalInstructions}\n" else ""} + """.trimMargin() + ), api = api + ) + var markdown = ui.socketManager?.addApplyFileDiffLinks( + root = root.toPath(), + response = response, + ui = ui, + api = api, + shouldAutoApply = { path -> + if (autoFix && !changed.contains(path)) { + changed.add(path) + true + } else { + false + } + } + ) + content.clear() + content.append("
${MarkdownUtil.renderMarkdown(markdown!!)}
") + } + +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanAheadApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanAheadApp.kt new file mode 100644 index 00000000..92383378 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanAheadApp.kt @@ -0,0 +1,66 @@ +package com.simiacryptus.skyenet.apps.general + +import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.models.OpenAITextModel +import com.simiacryptus.skyenet.apps.plan.PlanCoordinator +import com.simiacryptus.skyenet.apps.plan.Settings +import com.simiacryptus.skyenet.core.platform.ApplicationServices +import com.simiacryptus.skyenet.core.platform.ClientManager +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.application.ApplicationServer +import org.slf4j.LoggerFactory +import java.io.File + +class PlanAheadApp( + applicationName: String = "Task Planning v1.1", + path: String = "/taskDev", + val rootFile: File?, + val settings: Settings, + val model: OpenAITextModel, + val parsingModel: OpenAITextModel, + showMenubar: Boolean = true, +) : ApplicationServer( + applicationName = applicationName, + path = path, + showMenubar = showMenubar, +) { + override val root: File get() = rootFile ?: super.root + override val settingsClass: Class<*> get() = Settings::class.java + + @Suppress("UNCHECKED_CAST") + override fun initSettings(session: Session): T = settings.let { + if (null == rootFile) it.copy(workingDir = root.absolutePath) else + it + } as T + + override fun userMessage( + session: Session, + user: User?, + userMessage: String, + ui: ApplicationInterface, + api: API + ) { + try { + val settings = getSettings(session, user) + if (api is ClientManager.MonitoredClient) api.budget = settings?.budget ?: 2.0 + PlanCoordinator( + user = user, + session = session, + dataStorage = dataStorage, + api = api, + ui = ui, + root = (rootFile ?: dataStorage.getDataDir(user, session)).toPath(), + settings = settings!! + ).startProcess(userMessage = userMessage) + } catch (e: Throwable) { + ui.newTask().error(ui, e) + log.warn("Error", e) + } + } + + companion object { + private val log = LoggerFactory.getLogger(PlanAheadApp::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt index d2caabae..0f4d1dd1 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt @@ -90,87 +90,87 @@ class WebDevAgent( // parserClass = PageResourceListParser::class.java, resultClass = ProjectSpec::class.java, prompt = """ - Translate the user's idea into a detailed architecture for a simple web application. - - List all html, css, javascript, and image files to be created, and for each file: - 1. Mark with filename tags. - 2. Describe the public interface / interaction with other components. - 3. Core functional requirements. - - Specify user interactions and how the application will respond to them. - Identify key HTML classes and element IDs that will be used to bind the application to the HTML. - """.trimIndent(), + |Translate the user's idea into a detailed architecture for a simple web application. + | + | List all html, css, javascript, and image files to be created, and for each file: + | 1. Mark with filename tags. + | 2. Describe the public interface / interaction with other components. + | 3. Core functional requirements. + | + |Specify user interactions and how the application will respond to them. + |Identify key HTML classes and element IDs that will be used to bind the application to the HTML. + """.trimMargin(), model = model, parsingModel = parsingModel, ), ActorTypes.CodeReviewer to SimpleActor( prompt = """ - |Analyze the code summarized in the user's header-labeled code blocks. - |Review, look for bugs, and provide fixes. - |Provide implementations for missing functions. - | - |Response should use one or more code patches in diff format within ```diff code blocks. - |Each diff should be preceded by a header that identifies the file being modified. - |The diff format should use + for line additions, - for line deletions. - |The diff should include 2 lines of context before and after every change. - | - |Example: - | - |Here are the patches: - | - |### src/utils/exampleUtils.js - |```diff - | // Utility functions for example feature - | const b = 2; - | function exampleFunction() { - |- return b + 1; - |+ return b + 2; - | } - |``` - | - |### tests/exampleUtils.test.js - |```diff - | // Unit tests for exampleUtils - | const assert = require('assert'); - | const { exampleFunction } = require('../src/utils/exampleUtils'); - | - | describe('exampleFunction', () => { - |- it('should return 3', () => { - |+ it('should return 4', () => { - | assert.equal(exampleFunction(), 3); - | }); - | }); - |``` - """.trimMargin(), + |Analyze the code summarized in the user's header-labeled code blocks. + |Review, look for bugs, and provide fixes. + |Provide implementations for missing functions. + | + |Response should use one or more code patches in diff format within ```diff code blocks. + |Each diff should be preceded by a header that identifies the file being modified. + |The diff format should use + for line additions, - for line deletions. + |The diff should include 2 lines of context before and after every change. + | + |Example: + | + |Here are the patches: + | + |### src/utils/exampleUtils.js + |```diff + | // Utility functions for example feature + | const b = 2; + | function exampleFunction() { + |- return b + 1; + |+ return b + 2; + | } + |``` + | + |### tests/exampleUtils.test.js + |```diff + | // Unit tests for exampleUtils + | const assert = require('assert'); + | const { exampleFunction } = require('../src/utils/exampleUtils'); + | + | describe('exampleFunction', () => { + |- it('should return 3', () => { + |+ it('should return 4', () => { + | assert.equal(exampleFunction(), 3); + | }); + | }); + |``` + """.trimMargin(), model = model, ), ActorTypes.HtmlCodingActor to SimpleActor( prompt = """ - You will translate the user request into a skeleton HTML file for a rich javascript application. - The html file can reference needed CSS and JS files, which are will be located in the same directory as the html file. - Do not output the content of the resource files, only the html file. - """.trimIndent(), model = model + |You will translate the user request into a skeleton HTML file for a rich javascript application. + |The html file can reference needed CSS and JS files, which are will be located in the same directory as the html file. + |Do not output the content of the resource files, only the html file. + """.trimMargin(), model = model ), ActorTypes.JavascriptCodingActor to SimpleActor( prompt = """ - You will translate the user request into a javascript file for use in a rich javascript application. - """.trimIndent(), model = model + |You will translate the user request into a javascript file for use in a rich javascript application. + """.trimMargin(), model = model ), ActorTypes.CssCodingActor to SimpleActor( prompt = """ - You will translate the user request into a CSS file for use in a rich javascript application. - """.trimIndent(), model = model + |You will translate the user request into a CSS file for use in a rich javascript application. + """.trimMargin(), model = model ), ActorTypes.EtcCodingActor to SimpleActor( prompt = """ - You will translate the user request into a file for use in a web application. - """.trimIndent(), + |You will translate the user request into a file for use in a web application. + """.trimMargin(), model = model, ), ActorTypes.ImageActor to ImageActor( prompt = """ - You will translate the user request into an image file for use in a web application. - """.trimIndent(), + |You will translate the user request into an image file for use in a web application. + """.trimMargin(), textModel = model, imageModel = ImageModels.DallE3, ), diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractTask.kt new file mode 100644 index 00000000..61f1bd9a --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractTask.kt @@ -0,0 +1,84 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.diff.FileValidationUtils +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.set +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.session.SessionTask +import java.io.File +import java.nio.file.Path + +abstract class AbstractTask( + val settings: Settings, + val task: PlanCoordinator.Task +) { + val outputFiles: List? = task.output_files + val inputFiles: List? = task.input_files + val taskDependencies: List? = task.task_dependencies + val description: String? = task.description + var state: TaskState? = TaskState.Pending + val codeFiles = mutableMapOf() + + open val root: Path + get() = File(settings.workingDir).toPath() + + enum class TaskState { + Pending, + InProgress, + Completed, + } + + fun getPriorCode(genState: PlanCoordinator.GenState) = taskDependencies?.joinToString("\n\n\n") { dependency -> + """ + |# $dependency + | + |${genState.taskResult[dependency] ?: ""} + """.trimMargin() + } ?: "" + + + fun getInputFileCode(): String = ((inputFiles ?: listOf()) + (outputFiles ?: listOf())) + .filter { FileValidationUtils.isLLMIncludable(root.toFile().resolve(it)) }.joinToString("\n\n") { + try { + """ + |# $it + | + |$TRIPLE_TILDE + |${codeFiles[File(it).toPath()] ?: root.resolve(it).toFile().readText()} + |$TRIPLE_TILDE + """.trimMargin() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error: root=$root ", e) + "" + } + } + + fun acceptButtonFooter(ui: ApplicationInterface, fn: () -> Unit): String { + val footerTask = ui.newTask(false) + lateinit var textHandle: StringBuilder + textHandle = footerTask.complete(ui.hrefLink("Accept", classname = "href-link cmd-button") { + try { + textHandle.set("""
Accepted
""") + footerTask.complete() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error", e) + } + fn() + })!! + return footerTask.placeholder + } + + + abstract fun promptSegment(): String + + abstract fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/CommandAutoFixTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/CommandAutoFixTask.kt new file mode 100644 index 00000000..b12a8d00 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/CommandAutoFixTask.kt @@ -0,0 +1,121 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.jopenai.OpenAIClient +import com.simiacryptus.skyenet.Retryable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.apps.general.CmdPatchApp +import com.simiacryptus.skyenet.apps.general.PatchApp +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.io.File +import java.util.concurrent.Semaphore + +class CommandAutoFixTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + override fun promptSegment(): String { + return """ + |CommandAutoFix - Run a command and automatically fix any issues that arise + | ** Specify the command to be executed and any additional instructions + | ** Provide the command arguments in the 'commandArguments' field + | ** List input files/tasks to be examined when fixing issues + | ** Available commands: + | ${settings.commandAutoFixCommands.joinToString("\n ") { "* ${File(it).name}" }} + """.trimMargin() + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val semaphore = Semaphore(0) + val onComplete = { + semaphore.release() + } + if (!agent.settings.enableCommandAutoFix) { + task.add("Command Auto Fix is disabled") + onComplete() + } else { + Retryable(agent.ui, task = task) { + val task = agent.ui.newTask(false).apply { it.append(placeholder) } + val alias = this.task.command?.first() + val commandAutoFixCommands = agent.settings.commandAutoFixCommands + val cmds = commandAutoFixCommands.filter { + File(it).name.startsWith(alias ?: "") + } + val executable = cmds.firstOrNull() + if (executable == null) { + throw IllegalArgumentException("Command not found: $alias") + } + val outputResult = CmdPatchApp( + root = agent.root, + session = agent.session, + settings = PatchApp.Settings( + executable = File(executable), + arguments = this.task.command?.drop(1)?.joinToString(" ") ?: "", + workingDirectory = agent.root.toFile(), + exitCodeOption = "nonzero", + additionalInstructions = "", + autoFix = agent.settings.autoFix + ), + api = agent.api as OpenAIClient, + virtualFiles = agent.virtualFiles, + model = agent.settings.model, + ).run( + ui = agent.ui, + task = task + ) + genState.taskResult[taskId] = "Command Auto Fix completed" + task.add(if (outputResult.exitCode == 0) { + if (agent.settings.autoFix) { + taskTabs.selectedTab += 1 + taskTabs.update() + onComplete() + MarkdownUtil.renderMarkdown("## Auto-applied Command Auto Fix\n", ui = agent.ui) + } else { + MarkdownUtil.renderMarkdown( + "## Command Auto Fix Result\n", + ui = agent.ui + ) + acceptButtonFooter( + agent.ui + ) { + taskTabs.selectedTab += 1 + taskTabs.update() + onComplete() + } + } + } else { + MarkdownUtil.renderMarkdown( + "## Command Auto Fix Failed\n", + ui = agent.ui + ) + acceptButtonFooter( + agent.ui + ) { + taskTabs.selectedTab += 1 + taskTabs.update() + onComplete() + } + }) + task.placeholder + } + } + try { + semaphore.acquire() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error", e) + } + PlanCoordinator.log.debug("Completed command auto fix: $taskId") + } + + companion object { + private val log = LoggerFactory.getLogger(CommandAutoFixTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/DocumentationTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/DocumentationTask.kt new file mode 100644 index 00000000..fdef326d --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/DocumentationTask.kt @@ -0,0 +1,89 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.skyenet.Retryable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.util.concurrent.Semaphore + +class DocumentationTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + override fun promptSegment(): String { + return """ + |Documentation - Generate documentation + | ** List input files/tasks to be examined + """.trimMargin() + } + + val documentationGeneratorActor by lazy { + SimpleActor( + name = "DocumentationGenerator", + prompt = """ + |Create detailed and clear documentation for the provided code, covering its purpose, functionality, inputs, outputs, and any assumptions or limitations. + |Use a structured and consistent format that facilitates easy understanding and navigation. + |Include code examples where applicable, and explain the rationale behind key design decisions and algorithm choices. + |Document any known issues or areas for improvement, providing guidance for future developers on how to extend or maintain the code. + """.trimMargin(), + model = settings.model, + temperature = settings.temperature, + ) + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val semaphore = Semaphore(0) + val onComplete = { + semaphore.release() + } + val process = { sb: StringBuilder -> + val docResult = documentationGeneratorActor.answer( + listOf( + userMessage, + plan.text, + getPriorCode(genState), + getInputFileCode(), + ).filter { it.isNotBlank() }, agent.api + ) + genState.taskResult[taskId] = docResult + if (agent.settings.autoFix) { + taskTabs.selectedTab += 1 + taskTabs.update() + task.complete() + onComplete() + MarkdownUtil.renderMarkdown("## Generated Documentation\n$docResult\nAuto-accepted", ui = agent.ui) + } else { + MarkdownUtil.renderMarkdown( + "## Generated Documentation\n$docResult", + ui = agent.ui + ) + acceptButtonFooter(agent.ui) { + taskTabs.selectedTab += 1 + taskTabs.update() + task.complete() + onComplete() + } + } + } + Retryable(agent.ui, task = task, process = process) + try { + semaphore.acquire() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error", e) + } + } + + companion object { + private val log = LoggerFactory.getLogger(DocumentationTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/EditFileTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/EditFileTask.kt new file mode 100644 index 00000000..42284e01 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/EditFileTask.kt @@ -0,0 +1,146 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.skyenet.Retryable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.util.concurrent.Semaphore + +class EditFileTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + val filePatcherActor by lazy { + SimpleActor( + name = "FilePatcher", + prompt = """ + |Generate a patch for an existing file to modify its functionality or fix issues based on the given requirements and context. + |Ensure the modifications are efficient, maintain readability, and adhere to coding standards. + |Carefully review the existing code and project structure to ensure the changes are consistent and do not introduce bugs. + |Consider the impact of the modifications on other parts of the codebase. + | + |Provide a summary of the changes made. + | + |Response should use one or more code patches in diff format within ${TRIPLE_TILDE}diff code blocks. + |Each diff should be preceded by a header that identifies the file being modified. + |The diff format should use + for line additions, - for line deletions. + |The diff should include 2 lines of context before and after every change. + | + |Example: + | + |Here are the patches: + | + |### src/utils/exampleUtils.js + |${TRIPLE_TILDE}diff + | // Utility functions for example feature + | const b = 2; + | function exampleFunction() { + |- return b + 1; + |+ return b + 2; + | } + |$TRIPLE_TILDE + | + |### tests/exampleUtils.test.js + |${TRIPLE_TILDE}diff + | // Unit tests for exampleUtils + | const assert = require('assert'); + | const { exampleFunction } = require('../src/utils/exampleUtils'); + | + | describe('exampleFunction', () => { + |- it('should return 3', () => { + |+ it('should return 4', () => { + | assert.equal(exampleFunction(), 3); + | }); + | }); + |$TRIPLE_TILDE + """.trimMargin(), + model = settings.model, + temperature = settings.temperature, + ) + } + + override fun promptSegment(): String { + return """ + |EditFile - Modify existing files + | ** For each file, specify the relative file path and the goal of the modification + | ** List input files/tasks to be examined when designing the modifications + """.trimMargin() + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val semaphore = Semaphore(0) + val onComplete = { semaphore.release() } + val process = { sb: StringBuilder -> + val codeResult = filePatcherActor.answer( + listOf( + userMessage, + plan.text, + getPriorCode(genState), + getInputFileCode(), + this.description ?: "", + ).filter { it.isNotBlank() }, agent.api + ) + genState.taskResult[taskId] = codeResult + if (agent.settings.autoFix) { + val diffLinks = agent.ui.socketManager!!.addApplyFileDiffLinks( + root = agent.root, + response = codeResult, + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + task.complete("$path Updated") + } + }, + ui = agent.ui, + api = agent.api, + shouldAutoApply = { true } + ) + taskTabs.selectedTab += 1 + taskTabs.update() + task.complete() + onComplete() + MarkdownUtil.renderMarkdown(diffLinks + "\n\n## Auto-applied changes", ui = agent.ui) + } else { + MarkdownUtil.renderMarkdown( + agent.ui.socketManager!!.addApplyFileDiffLinks( + root = agent.root, + response = codeResult, + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + task.complete("$path Updated") + } + }, + ui = agent.ui, + api = agent.api + ) + acceptButtonFooter(agent.ui) { + taskTabs.selectedTab += 1 + taskTabs.update() + task.complete() + onComplete() + }, ui = agent.ui + ) + } + } + Retryable(agent.ui, task = task, process = process) + try { + semaphore.acquire() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error", e) + } + } + + companion object { + private val log = LoggerFactory.getLogger(EditFileTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/InquiryTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/InquiryTask.kt new file mode 100644 index 00000000..009cc3da --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/InquiryTask.kt @@ -0,0 +1,95 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import com.simiacryptus.jopenai.util.JsonUtil +import com.simiacryptus.skyenet.Discussable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicReference + +class InquiryTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + val inquiryActor by lazy { + SimpleActor( + name = "Inquiry", + prompt = """ + |Create code for a new file that fulfills the specified requirements and context. + |Given a detailed user request, break it down into smaller, actionable tasks suitable for software development. + |Compile comprehensive information and insights on the specified topic. + |Provide a comprehensive overview, including key concepts, relevant technologies, best practices, and any potential challenges or considerations. + |Ensure the information is accurate, up-to-date, and well-organized to facilitate easy understanding. + + |When generating insights, consider the existing project context and focus on information that is directly relevant and applicable. + |Focus on generating insights and information that support the task types available in the system (Requirements, NewFile, EditFile, ${ + if (!settings.taskPlanningEnabled) "" else "TaskPlanning, " + }${ + if (!settings.shellCommandTaskEnabled) "" else "RunShellCommand, " + }Documentation). + |This will ensure that the inquiries are tailored to assist in the planning and execution of tasks within the system's framework. + """.trimMargin(), + model = settings.model, + temperature = settings.temperature, + ) + } + + override fun promptSegment(): String { + return """ + |Inquiry - Answer questions by reading in files and providing a summary that can be discussed with and approved by the user + | ** Specify the questions and the goal of the inquiry + | ** List input files to be examined when answering the questions + """.trimMargin() + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val toInput = { it: String -> + listOf( + userMessage, + plan.text, + getPriorCode(genState), + getInputFileCode(), + it, + ).filter { it.isNotBlank() } + } + val inquiryResult = Discussable( + task = task, + userMessage = { "Expand ${this.description ?: ""}\n${JsonUtil.toJson(data = this)}" }, + heading = "", + initialResponse = { it: String -> inquiryActor.answer(toInput(it), api = agent.api) }, + outputFn = { design: String -> + MarkdownUtil.renderMarkdown(design, ui = agent.ui) + }, + ui = agent.ui, + reviseResponse = { userMessages: List> -> + inquiryActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput("Expand ${this.description ?: ""}\n${JsonUtil.toJson(data = this)}"), + api = agent.api + ) + }, + atomicRef = AtomicReference(), + semaphore = Semaphore(0), + ).call() + genState.taskResult[taskId] = inquiryResult + } + + companion object { + private val log = LoggerFactory.getLogger(InquiryTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/NewFileTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/NewFileTask.kt new file mode 100644 index 00000000..f429fa60 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/NewFileTask.kt @@ -0,0 +1,138 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.skyenet.Retryable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.util.concurrent.Semaphore + +class NewFileTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + val newFileCreatorActor by lazy { + SimpleActor( + name = "NewFileCreator", + prompt = """ + |Generate the necessary code for new files based on the given requirements and context. + |For each file: + |Provide a clear relative file path based on the content and purpose of the file. + |Ensure the code is well-structured, follows best practices, and meets the specified functionality. + |Carefully consider how the new file fits into the existing project structure and architecture. + |Avoid creating files that duplicate functionality or introduce inconsistencies. + | + |The response format should be as follows: + |- Use triple backticks to create code blocks for each file. + |- Each code block should be preceded by a header specifying the file path. + |- The file path should be a relative path from the project root. + |- Separate code blocks with a single blank line. + |- Specify the language for syntax highlighting after the opening triple backticks. + | + |Example: + | + |Here are the new files: + | + |### src/utils/exampleUtils.js + |${TRIPLE_TILDE}js + |// Utility functions for example feature + |const b = 2; + |function exampleFunction() { + | return b + 1; + |} + | + |$TRIPLE_TILDE + | + |### tests/exampleUtils.test.js + |${TRIPLE_TILDE}js + |// Unit tests for exampleUtils + |const assert = require('assert'); + |const { exampleFunction } = require('../src/utils/exampleUtils'); + | + |describe('exampleFunction', () => { + | it('should return 3', () => { + | assert.equal(exampleFunction(), 3); + | }); + |}); + $TRIPLE_TILDE + """.trimMargin(), + model = settings.model, + temperature = settings.temperature, + ) + } + + override fun promptSegment(): String { + return """ + |NewFile - Create one or more new files, carefully considering how they fit into the existing project structure + | ** For each file, specify the relative file path and the purpose of the file + | ** List input files/tasks to be examined when authoring the new files + """.trimMargin() + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val semaphore = Semaphore(0) + val onComplete = { semaphore.release() } + val process = { sb: StringBuilder -> + val codeResult = newFileCreatorActor.answer( + listOf( + userMessage, + plan.text, + getPriorCode(genState), + getInputFileCode(), + this.description ?: "", + ).filter { it.isNotBlank() }, agent.api + ) + genState.taskResult[taskId] = codeResult + if (agent.settings.autoFix) { + val diffLinks = agent.ui.socketManager!!.addApplyFileDiffLinks( + agent.root, + codeResult, + api = agent.api, + ui = agent.ui, + shouldAutoApply = { true }) + taskTabs.selectedTab += 1 + taskTabs.update() + onComplete() + MarkdownUtil.renderMarkdown(diffLinks + "\n\n## Auto-applied changes", ui = agent.ui) + } else { + MarkdownUtil.renderMarkdown( + agent.ui.socketManager!!.addApplyFileDiffLinks( + agent.root, + codeResult, + api = agent.api, + ui = agent.ui + ), + ui = agent.ui + ) + acceptButtonFooter(agent.ui) { + taskTabs.selectedTab += 1 + taskTabs.update() + onComplete() + } + } + } + Retryable( + agent.ui, task = task, + process = process + ) + try { + semaphore.acquire() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error", e) + } + } + + companion object { + private val log = LoggerFactory.getLogger(NewFileTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanCoordinator.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanCoordinator.kt new file mode 100644 index 00000000..8dc0cccd --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanCoordinator.kt @@ -0,0 +1,392 @@ +package com.simiacryptus.skyenet.apps.plan + + +import com.simiacryptus.diff.FileValidationUtils +import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.describe.Description +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import com.simiacryptus.jopenai.util.JsonUtil +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.Discussable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.core.platform.ApplicationServices +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.StorageInterface +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.set +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Path +import java.util.* +import java.util.concurrent.Future +import java.util.concurrent.ThreadPoolExecutor + +class PlanCoordinator( + val user: User?, + val session: Session, + val dataStorage: StorageInterface, + val ui: ApplicationInterface, + val api: API, + val settings: Settings, + val root: Path +) { + private val taskBreakdownActor by lazy { settings.planningActor() } + + data class TaskBreakdownResult( + val tasksByID: Map? = null, + val finalTaskID: String? = null, + ) + + val pool: ThreadPoolExecutor by lazy { ApplicationServices.clientManager.getPool(session, user) } + + data class Task( + val description: String? = null, + val taskType: TaskType? = null, + var task_dependencies: List? = null, + val input_files: List? = null, + val output_files: List? = null, + var state: AbstractTask.TaskState? = null, + @Description("Command and arguments (in list form) for the task") + val command: List? = null, + ) + + val virtualFiles: Array by lazy { + FileValidationUtils.expandFileList(root.toFile()) + } + + private val codeFiles: Map + get() = virtualFiles + .filter { it.exists() && it.isFile } + .filter { !it.name.startsWith(".") } + .associate { file -> getKey(file) to getValue(file) } + + + private fun getValue(file: File) = try { + file.inputStream().bufferedReader().use { it.readText() } + } catch (e: Exception) { + log.warn("Error reading file", e) + "" + } + + private fun getKey(file: File) = root.relativize(file.toPath()) + + fun startProcess(userMessage: String) { + val codeFiles = codeFiles + val eventStatus = if (!codeFiles.all { it.key.toFile().isFile } || codeFiles.size > 2) """ + Files: + ${codeFiles.keys.joinToString("\n") { "* $it" }} + """.trimMargin() else { + """ + |${ + virtualFiles.joinToString("\n\n") { + val path = root.relativize(it.toPath()) + """ + ## $path + | + ${(codeFiles[path] ?: "").let { "$TRIPLE_TILDE\n${it/*.indent(" ")*/}\n$TRIPLE_TILDE" }} + """.trimMargin() + } + } + """.trimMargin() + } + val task = ui.newTask() + val toInput = { it: String -> + listOf( + eventStatus, + it + ) + } + val highLevelPlan = Discussable( + task = task, + heading = MarkdownUtil.renderMarkdown(userMessage, ui = ui), + userMessage = { userMessage }, + initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, + outputFn = { design: ParsedResponse -> + AgentPatterns.displayMapInTabs( + mapOf( + "Text" to MarkdownUtil.renderMarkdown(design.text, ui = ui), + "JSON" to MarkdownUtil.renderMarkdown( + "${TRIPLE_TILDE}json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\n$TRIPLE_TILDE", + ui = ui + ), + ) + ) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + taskBreakdownActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput(userMessage), + api = api + ) + }, + ).call() + + initPlan(highLevelPlan, userMessage, task) + } + + fun initPlan( + plan: ParsedResponse, + userMessage: String, + task: SessionTask + ) { + try { + val tasksByID = + plan.obj.tasksByID?.entries?.toTypedArray()?.associate { it.key to it.value } ?: mapOf() + val genState = GenState(tasksByID.toMutableMap()) + val diagramTask = ui.newTask(false).apply { task.add(placeholder) } + val diagramBuffer = + diagramTask.add( + MarkdownUtil.renderMarkdown( + "## Task Dependency Graph\n${TRIPLE_TILDE}mermaid\n${buildMermaidGraph(genState.subTasks)}\n$TRIPLE_TILDE", + ui = ui + ) + ) + val taskIdProcessingQueue = genState.taskIdProcessingQueue + val subTasks = genState.subTasks + executePlan( + task, + diagramBuffer, + subTasks, + diagramTask, + genState, + taskIdProcessingQueue, + pool, + userMessage, + plan + ) + } catch (e: Throwable) { + log.warn("Error during incremental code generation process", e) + task.error(ui, e) + } + } + + fun executePlan( + task: SessionTask, + diagramBuffer: StringBuilder?, + subTasks: Map, + diagramTask: SessionTask, + genState: GenState, + taskIdProcessingQueue: MutableList, + pool: ThreadPoolExecutor, + userMessage: String, + plan: ParsedResponse + ) { + val taskTabs = object : TabbedDisplay(ui.newTask(false).apply { task.add(placeholder) }) { + override fun renderTabButtons(): String { + diagramBuffer?.set( + MarkdownUtil.renderMarkdown( + """ + |## Task Dependency Graph + |${TRIPLE_TILDE}mermaid + |${buildMermaidGraph(subTasks)} + |$TRIPLE_TILDE + """.trimMargin(), ui = ui + ) + ) + diagramTask.complete() + return buildString { + append("
\n") + super.tabs.withIndex().forEach { (idx, t) -> + val (taskId, taskV) = t + val subTask = genState.tasksByDescription[taskId] + if (null == subTask) { + log.warn("Task tab not found: $taskId") + } + val isChecked = if (taskId in taskIdProcessingQueue) "checked" else "" + val style = when (subTask?.state) { + AbstractTask.TaskState.Completed -> " style='text-decoration: line-through;'" + null -> " style='opacity: 20%;'" + AbstractTask.TaskState.Pending -> " style='opacity: 30%;'" + else -> "" + } + append("
\n") + } + append("
") + } + } + } + // Initialize task tabs + taskIdProcessingQueue.forEach { taskId -> + val newTask = ui.newTask(false) + genState.uitaskMap[taskId] = newTask + val subtask = genState.subTasks[taskId] + val description = subtask?.description + log.debug("Creating task tab: $taskId ${System.identityHashCode(subtask)} $description") + taskTabs[description ?: taskId] = newTask.placeholder + } + Thread.sleep(100) + while (taskIdProcessingQueue.isNotEmpty()) { + val taskId = taskIdProcessingQueue.removeAt(0) + val subTask = genState.subTasks[taskId] ?: throw RuntimeException("Task not found: $taskId") + genState.taskFutures[taskId] = pool.submit { + subTask.state = AbstractTask.TaskState.Pending + taskTabs.update() + log.debug("Awaiting dependencies: ${subTask.task_dependencies?.joinToString(", ") ?: ""}") + subTask.task_dependencies + ?.associate { it to genState.taskFutures[it] } + ?.forEach { (id, future) -> + try { + future?.get() ?: log.warn("Dependency not found: $id") + } catch (e: Throwable) { + log.warn("Error", e) + } + } + subTask.state = AbstractTask.TaskState.InProgress + taskTabs.update() + log.debug("Running task: ${System.identityHashCode(subTask)} ${subTask.description}") + val task1 = genState.uitaskMap.get(taskId) ?: ui.newTask(false).apply { + taskTabs[taskId] = placeholder + } + try { + val dependencies = subTask.task_dependencies?.toMutableSet() ?: mutableSetOf() + dependencies += getAllDependencies( + subTask = subTask, + subTasks = genState.subTasks, + visited = mutableSetOf() + ) + + task1.add( + MarkdownUtil.renderMarkdown( + """ + ## Task `${taskId}` + ${subTask.description ?: ""} + | + |${TRIPLE_TILDE}json + |${JsonUtil.toJson(data = subTask)/*.indent(" ")*/} + |$TRIPLE_TILDE + | + |### Dependencies: + |${dependencies.joinToString("\n") { "- $it" }} + | + """.trimMargin(), ui = ui + ) + ) + settings.getImpl(subTask).run( + agent = this, + taskId = taskId, + userMessage = userMessage, + plan = plan, + genState = genState, + task = task1, + taskTabs = taskTabs + ) + } catch (e: Throwable) { + log.warn("Error during task execution", e) + task1.error(ui, e) + } finally { + genState.completedTasks.add(element = taskId) + subTask.state = AbstractTask.TaskState.Completed + log.debug("Completed task: $taskId ${System.identityHashCode(subTask)}") + taskTabs.update() + } + } + } + genState.taskFutures.forEach { (id, future) -> + try { + future.get() ?: log.warn("Dependency not found: $id") + } catch (e: Throwable) { + log.warn("Error", e) + } + } + } + + private fun getAllDependencies( + subTask: Task, + subTasks: Map, + visited: MutableSet + ): List { + val dependencies = subTask.task_dependencies?.toMutableList() ?: mutableListOf() + subTask.task_dependencies?.forEach { dep -> + if (dep in visited) return@forEach + val subTask = subTasks[dep] + if (subTask != null) { + visited.add(dep) + dependencies.addAll(getAllDependencies(subTask, subTasks, visited)) + } + } + return dependencies + } + + companion object { + val log = LoggerFactory.getLogger(PlanCoordinator::class.java) + + fun executionOrder(tasks: Map): List { + val taskIds: MutableList = mutableListOf() + val taskMap = tasks.toMutableMap() + while (taskMap.isNotEmpty()) { + val nextTasks = + taskMap.filter { (_, task) -> task.task_dependencies?.all { taskIds.contains(it) } ?: true } + if (nextTasks.isEmpty()) { + throw RuntimeException("Circular dependency detected in task breakdown") + } + taskIds.addAll(nextTasks.keys) + nextTasks.keys.forEach { taskMap.remove(it) } + } + return taskIds + } + + val isWindows = System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows") + private fun sanitizeForMermaid(input: String) = input + .replace(" ", "_") + .replace("\"", "\\\"") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("(", "\\(") + .replace(")", "\\)") + .let { "`$it`" } + + private fun escapeMermaidCharacters(input: String) = input + .replace("\"", "\\\"") + .let { '"' + it + '"' } + + fun buildMermaidGraph(subTasks: Map): String { + val graphBuilder = StringBuilder("graph TD;\n") + subTasks.forEach { (taskId, task) -> + val sanitizedTaskId = sanitizeForMermaid(taskId) + val taskType = task.taskType?.name ?: "Unknown" + val escapedDescription = escapeMermaidCharacters(task.description ?: "") + val style = when (task.state) { + AbstractTask.TaskState.Completed -> ":::completed" + AbstractTask.TaskState.InProgress -> ":::inProgress" + else -> ":::$taskType" + } + graphBuilder.append(" ${sanitizedTaskId}[$escapedDescription]$style;\n") + task.task_dependencies?.forEach { dependency -> + val sanitizedDependency = sanitizeForMermaid(dependency) + graphBuilder.append(" $sanitizedDependency --> ${sanitizedTaskId};\n") + } + } + graphBuilder.append(" classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef NewFile fill:lightblue,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef EditFile fill:lightgreen,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef Documentation fill:lightyellow,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef Inquiry fill:orange,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef TaskPlanning fill:lightgrey,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef completed fill:#90EE90,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef inProgress fill:#FFA500,stroke:#333,stroke-width:2px;\n") + return graphBuilder.toString() + } + + } + + data class GenState( + val subTasks: Map, + val tasksByDescription: MutableMap = subTasks.entries.toTypedArray() + .associate { it.value.description to it.value }.toMutableMap(), + val taskIdProcessingQueue: MutableList = executionOrder(subTasks).toMutableList(), + val taskResult: MutableMap = mutableMapOf(), + val completedTasks: MutableList = mutableListOf(), + val taskFutures: MutableMap> = mutableMapOf(), + val uitaskMap: MutableMap = mutableMapOf() + ) + +} + +const val TRIPLE_TILDE = "```" \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanningTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanningTask.kt new file mode 100644 index 00000000..447a2316 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanningTask.kt @@ -0,0 +1,119 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import com.simiacryptus.jopenai.util.JsonUtil +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.Discussable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.apps.plan.PlanCoordinator.Companion.buildMermaidGraph +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory + +class PlanningTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + val taskBreakdownActor by lazy { settings.planningActor() } + + override fun promptSegment(): String { + return """ + |TaskPlanning - High-level planning and organization of tasks - identify smaller, actionable tasks based on the information available at task execution time. + | ** Specify the prior tasks and the goal of the task + | ** Used to dynamically break down tasks as needed given new information + | ** Important: A planning task should not be used to begin a plan, as no new knowledge will be present + """.trimMargin() + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + if (!agent.settings.taskPlanningEnabled) throw RuntimeException("Task planning is disabled") + @Suppress("NAME_SHADOWING") val task = agent.ui.newTask(false).apply { task.add(placeholder) } + fun toInput(s: String) = listOf( + userMessage, + plan.text, + getPriorCode(genState), + getInputFileCode(), + s + ).filter { it.isNotBlank() } + + val subPlan = Discussable( + task = task, + userMessage = { "Expand ${description ?: ""}\n${JsonUtil.toJson(task)}" }, + heading = "", + initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = agent.api) }, + outputFn = { design: ParsedResponse -> + AgentPatterns.displayMapInTabs( + mapOf( + "Text" to MarkdownUtil.renderMarkdown(design.text, ui = agent.ui), + "JSON" to MarkdownUtil.renderMarkdown( + "${TRIPLE_TILDE}json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\n$TRIPLE_TILDE", + ui = agent.ui + ), + ) + ) + }, + ui = agent.ui, + reviseResponse = { userMessages: List> -> + taskBreakdownActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput("Expand ${description ?: ""}\n${JsonUtil.toJson(this)}"), + api = agent.api + ) + }, + ).call() + // Execute sub-tasks + executeSubTasks(agent, userMessage, subPlan, task) + } + private fun executeSubTasks( + agent: PlanCoordinator, + userMessage: String, + subPlan: ParsedResponse, + parentTask: SessionTask + ) { + val subPlanTask = agent.ui.newTask(false) + parentTask.add(subPlanTask.placeholder) + val subTasks = subPlan.obj.tasksByID ?: emptyMap() + val genState = PlanCoordinator.GenState(subTasks.toMutableMap()) + agent.executePlan( + task = subPlanTask, + diagramBuffer = subPlanTask.add(diagram(genState, agent.ui)), + subTasks = subTasks, + diagramTask = subPlanTask, + genState = genState, + taskIdProcessingQueue = PlanCoordinator.executionOrder(subTasks).toMutableList(), + pool = agent.pool, + userMessage = userMessage, + plan = subPlan, + ) + subPlanTask.complete() + } + + private fun diagram( + genState: PlanCoordinator.GenState, + ui: ApplicationInterface + ) = MarkdownUtil.renderMarkdown( + """ + |## Sub-Plan Task Dependency Graph + |${TRIPLE_TILDE}mermaid + |${buildMermaidGraph(genState.subTasks)} + |$TRIPLE_TILDE + """.trimMargin(), + ui = ui + ) + + companion object { + private val log = LoggerFactory.getLogger(PlanningTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt new file mode 100644 index 00000000..7244715b --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt @@ -0,0 +1,135 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.apps.coding.CodingAgent +import com.simiacryptus.skyenet.core.actors.CodingActor +import com.simiacryptus.skyenet.core.actors.ParsedResponse +import com.simiacryptus.skyenet.interpreter.ProcessInterpreter +import com.simiacryptus.skyenet.webui.session.SessionTask +import org.slf4j.LoggerFactory +import java.io.File +import java.util.concurrent.Semaphore +import kotlin.reflect.KClass + +class RunShellCommandTask( + settings: Settings, + task: PlanCoordinator.Task +) : AbstractTask(settings, task) { + val shellCommandActor by lazy { + CodingActor( + name = "RunShellCommand", + interpreterClass = ProcessInterpreter::class, + details = """ + |Execute the following shell command(s) and provide the output. Ensure to handle any errors or exceptions gracefully. + | + |Note: This task is for running simple and safe commands. Avoid executing commands that can cause harm to the system or compromise security. + """.trimMargin(), + symbols = mapOf( + "env" to settings.env, + "workingDir" to File(settings.workingDir).absolutePath, + "language" to settings.language, + "command" to settings.command, + ), + model = settings.model, + temperature = settings.temperature, + ) + } + + override fun promptSegment(): String { + return """ + |RunShellCommand - Execute shell commands and provide the output + | ** Specify the command to be executed, or describe the task to be performed + | ** List input files/tasks to be examined when writing the command + """.trimMargin() + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: ParsedResponse, + genState: PlanCoordinator.GenState, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + if (!agent.settings.shellCommandTaskEnabled) throw RuntimeException("Shell command task is disabled") + val semaphore = Semaphore(0) + object : CodingAgent( + api = agent.api, + dataStorage = agent.dataStorage, + session = agent.session, + user = agent.user, + ui = agent.ui, + interpreter = shellCommandActor.interpreterClass as KClass, + symbols = shellCommandActor.symbols, + temperature = shellCommandActor.temperature, + details = shellCommandActor.details, + model = shellCommandActor.model, + mainTask = task, + ) { + override fun displayFeedback( + task: SessionTask, + request: CodingActor.CodeRequest, + response: CodingActor.CodeResult + ) { + val formText = StringBuilder() + var formHandle: StringBuilder? = null + formHandle = task.add( + """ + |
+ |${if (!super.canPlay) "" else super.playButton(task, request, response, formText) { formHandle!! }} + |${acceptButton(response)} + |
+ |${super.reviseMsg(task, request, response, formText) { formHandle!! }} + """.trimMargin(), className = "reply-message" + ) + formText.append(formHandle.toString()) + formHandle.toString() + task.complete() + } + + fun acceptButton( + response: CodingActor.CodeResult + ): String { + return ui.hrefLink("Accept", "href-link play-button") { + genState.taskResult[taskId] = response.let { + """ + |## Shell Command Output + | + |$TRIPLE_TILDE + |${response.code} + |$TRIPLE_TILDE + | + |$TRIPLE_TILDE + |${response.renderedResponse} + |$TRIPLE_TILDE + """.trimMargin() + } + semaphore.release() + } + } + }.apply> { + start( + codeRequest( + listOf>( + userMessage to ApiModel.Role.user, + plan.text to ApiModel.Role.assistant, + getPriorCode(genState) to ApiModel.Role.assistant, + getInputFileCode() to ApiModel.Role.assistant, + ) + ) + ) + } + try { + semaphore.acquire() + } catch (e: Throwable) { + PlanCoordinator.log.warn("Error", e) + } + PlanCoordinator.log.debug("Completed shell command: $taskId") + } + + companion object { + private val log = LoggerFactory.getLogger(RunShellCommandTask::class.java) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/Settings.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/Settings.kt new file mode 100644 index 00000000..ca634b0a --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/Settings.kt @@ -0,0 +1,63 @@ +package com.simiacryptus.skyenet.apps.plan + +import com.simiacryptus.skyenet.apps.plan.PlanCoordinator.Task +import com.simiacryptus.skyenet.apps.plan.PlanCoordinator.TaskBreakdownResult +import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAITextModel +import com.simiacryptus.skyenet.core.actors.ParsedActor + +data class Settings( + val model: OpenAITextModel, + val parsingModel: OpenAITextModel, + val command: List, + val temperature: Double = 0.2, + val budget: Double = 2.0, + val taskPlanningEnabled: Boolean = false, + val shellCommandTaskEnabled: Boolean = true, + val autoFix: Boolean = false, + val enableCommandAutoFix: Boolean = false, + var commandAutoFixCommands: List = listOf(), + val env: Map = mapOf(), + val workingDir: String = ".", + val language: String = if (PlanCoordinator.isWindows) "powershell" else "bash", +) { + fun getImpl(task: Task): AbstractTask { + return when (task.taskType) { + TaskType.TaskPlanning -> PlanningTask(this, task) + TaskType.Documentation -> DocumentationTask(this, task) + TaskType.NewFile -> NewFileTask(this, task) + TaskType.EditFile -> EditFileTask(this, task) + TaskType.RunShellCommand -> RunShellCommandTask(this, task) + TaskType.CommandAutoFix -> CommandAutoFixTask(this, task) + TaskType.Inquiry -> InquiryTask(this, task) + else -> throw RuntimeException("Unknown task type: ${task.taskType}") + } + } + + fun planningActor() = ParsedActor( + name = "TaskBreakdown", + resultClass = TaskBreakdownResult::class.java, + prompt = """ + |Given a user request, identify and list smaller, actionable tasks that can be directly implemented in code. + |Detail files input and output as well as task execution dependencies. + |Creating directories and initializing source control are out of scope. + | + |Tasks can be of the following types: + | + |${getAvailableTaskTypes().joinToString("\n") { "* ${it.promptSegment()}" }} + | + |${if (taskPlanningEnabled) "Do not start your plan with a plan to plan!\n" else ""} + """.trimMargin(), + model = this.model, + parsingModel = this.parsingModel, + temperature = this.temperature, + ) + + private fun getAvailableTaskTypes(): List = TaskType.values().filter { + when (it) { + TaskType.TaskPlanning -> this.taskPlanningEnabled + TaskType.RunShellCommand -> this.shellCommandTaskEnabled + else -> true + } + }.map { this.getImpl(Task(taskType = it)) } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/TaskType.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/TaskType.kt new file mode 100644 index 00000000..4066a329 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/TaskType.kt @@ -0,0 +1,11 @@ +package com.simiacryptus.skyenet.apps.plan + +enum class TaskType { + TaskPlanning, + Inquiry, + NewFile, + EditFile, + Documentation, + RunShellCommand, + CommandAutoFix, +} \ No newline at end of file diff --git a/webui/src/main/resources/application/appScript.js b/webui/src/main/resources/application/appScript.js deleted file mode 100644 index e69de29b..00000000 diff --git a/webui/src/main/resources/application/index.html b/webui/src/main/resources/application/index.html index 11c466cf..f67282f2 100644 --- a/webui/src/main/resources/application/index.html +++ b/webui/src/main/resources/application/index.html @@ -22,7 +22,6 @@ -
diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index 90ea7099..7adc8553 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -11,6 +11,8 @@ import { toggleVerbose } from './functions.js'; import {restoreTabs, updateTabs} from './tabs.js'; +console.log('Main script started'); + let messageVersions = {}; window.messageMap = {}; // Make messageMap global @@ -34,42 +36,49 @@ function debounce(func, wait) { } // Create a debounced version of updateDocumentComponents -const debouncedUpdateDocumentComponents = debounce(updateDocumentComponents, 250); +export const debouncedUpdateDocumentComponents = debounce(updateDocumentComponents, 250); -function updateDocumentComponents() { +export function updateDocumentComponents() { + try { + updateTabs(); + } catch (e) { + console.error("Error updating tabs:", e); + } try { if (typeof Prism !== 'undefined') Prism.highlightAll(); } catch (e) { - console.log("Error highlighting code: " + e); + console.error("Error highlighting code:", e); } try { refreshVerbose(); } catch (e) { - console.log("Error refreshing verbose: " + e); + console.error("Error refreshing verbose:", e); } try { refreshReplyForms() } catch (e) { - console.log("Error refreshing reply forms: " + e); + console.error("Error refreshing reply forms:", e); } try { - if (typeof mermaid !== 'undefined') mermaid.run(); + if (typeof mermaid !== 'undefined') { + const mermaidDiagrams = document.querySelectorAll('.mermaid:not(.mermaid-processed)'); + if (mermaidDiagrams.length > 0) { + mermaid.run(); + mermaidDiagrams.forEach(diagram => diagram.classList.add('mermaid-processed')); + } + } } catch (e) { - console.log("Error running mermaid: " + e); + console.error("Error running mermaid:", e); } try { applyToAllSvg(); } catch (e) { - console.log("Error applying SVG pan zoom: " + e); - } - try { - updateTabs(); - } catch (e) { - console.log("Error updating tabs: " + e); + console.error("Error applying SVG pan zoom:", e); } } function onWebSocketText(event) { + console.log('WebSocket message received:', event.data); console.debug('WebSocket message:', event); const messagesDiv = document.getElementById('messages'); if (!messagesDiv) return; @@ -82,6 +91,7 @@ function onWebSocketText(event) { window.messageMap[messageId] = messageContent; const messageDivs = document.querySelectorAll('[id="' + messageId + '"]'); + console.log(`Found ${messageDivs.length} message divs for messageId: ${messageId}`); messageDivs.forEach((messageDiv) => { if (messageDiv) { messageDiv.innerHTML = messageContent; @@ -90,6 +100,7 @@ function onWebSocketText(event) { } }); if (messageDivs.length === 0 && !messageId.startsWith("z")) { + console.log(`Creating new message div for messageId: ${messageId}`); messageDiv = document.createElement('div'); messageDiv.className = 'message message-container ' + (messageId.startsWith('u') ? 'user-message' : 'response-message'); messageDiv.id = messageId; @@ -122,13 +133,14 @@ function onWebSocketText(event) { } document.addEventListener('DOMContentLoaded', () => { + console.log('DOM content loaded'); if (typeof mermaid !== 'undefined') mermaid.run(); - applyToAllSvg(); - - // Set a timer to periodically apply svgPanZoom to all SVG elements - setInterval(() => { - applyToAllSvg(); - }, 5000); // Adjust the interval as needed + //applyToAllSvg(); + // + // // Set a timer to periodically apply svgPanZoom to all SVG elements + // setInterval(() => { + // applyToAllSvg(); + // }, 5000); // Adjust the interval as needed restoreTabs(); @@ -164,8 +176,10 @@ document.addEventListener('DOMContentLoaded', () => { const sessionId = getSessionId(); if (sessionId) { + console.log(`Connecting with session ID: ${sessionId}`); connect(sessionId, onWebSocketText); } else { + console.log('Connecting without session ID'); connect(undefined, onWebSocketText); } @@ -178,6 +192,7 @@ document.addEventListener('DOMContentLoaded', () => { if (form) form.addEventListener('submit', (event) => { event.preventDefault(); + console.log('Form submitted'); queueMessage(messageInput.value); messageInput.value = ''; @@ -195,6 +210,7 @@ document.addEventListener('DOMContentLoaded', () => { if (messageInput) { messageInput.addEventListener('keydown', (event) => { + console.log('Key pressed in message input:', event.key); if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); form.dispatchEvent(new Event('submit')); @@ -206,6 +222,7 @@ document.addEventListener('DOMContentLoaded', () => { let heightAdjustment = postEditScrollHeight - originalScrollHeight; messageInput.style.height = ''; messageInput.addEventListener('input', function () { + console.log('Input event on message input'); // Reset the height to a single row to get the scroll height for the current content this.style.height = 'auto'; // Set the height to the scroll height, which represents the height of the content @@ -235,29 +252,35 @@ document.addEventListener('DOMContentLoaded', () => { document.body.addEventListener('click', (event) => { const target = event.target; + console.log('Click event on body, target:', target); const hrefLink = findAncestor(target, '.href-link'); if (hrefLink) { const messageId = hrefLink.getAttribute('data-id'); + console.log('Href link clicked, messageId:', messageId); if (messageId && messageId !== '' && messageId !== null) queueMessage('!' + messageId + ',link'); } else { const playButton = findAncestor(target, '.play-button'); if (playButton) { const messageId = playButton.getAttribute('data-id'); + console.log('Play button clicked, messageId:', messageId); if (messageId && messageId !== '' && messageId !== null) queueMessage('!' + messageId + ',run'); } else { const regenButton = findAncestor(target, '.regen-button'); if (regenButton) { const messageId = regenButton.getAttribute('data-id'); + console.log('Regen button clicked, messageId:', messageId); if (messageId && messageId !== '' && messageId !== null) queueMessage('!' + messageId + ',regen'); } else { const cancelButton = findAncestor(target, '.cancel-button'); if (cancelButton) { const messageId = cancelButton.getAttribute('data-id'); + console.log('Cancel button clicked, messageId:', messageId); if (messageId && messageId !== '' && messageId !== null) queueMessage('!' + messageId + ',stop'); } else { const textSubmitButton = findAncestor(target, '.text-submit-button'); if (textSubmitButton) { const messageId = textSubmitButton.getAttribute('data-id'); + console.log('Text submit button clicked, messageId:', messageId); const text = document.querySelector('.reply-input[data-id="' + messageId + '"]').value; // url escape the text const escapedText = encodeURIComponent(text); @@ -272,6 +295,7 @@ document.addEventListener('DOMContentLoaded', () => { let filesElement = document.getElementById("files"); if (filesElement) filesElement.addEventListener("click", function (event) { event.preventDefault(); + console.log('Files element clicked'); const sessionId = getSessionId(); const url = "fileIndex/" + sessionId + "/"; window.open(url, "_blank"); @@ -279,12 +303,14 @@ document.addEventListener('DOMContentLoaded', () => { fetch('appInfo?session=' + sessionId) .then(response => { + console.log('AppInfo fetch response:', response); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { + console.log('AppInfo data:', data); if (data) { if (data.applicationName) { document.title = data.applicationName; @@ -325,12 +351,14 @@ document.addEventListener('DOMContentLoaded', () => { fetch('/userInfo') .then(response => { + console.log('UserInfo fetch response:', response); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { + console.log('UserInfo data:', data); if (data.name) { // Update the username link with the user's name and make it visible usernameLink.textContent = data.name; @@ -360,9 +388,11 @@ document.addEventListener('DOMContentLoaded', () => { // Restore the selected tabs from localStorage document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + console.log('Restoring tabs for container:', tabsContainer.id); const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); if (savedTab) { const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); +console.log('Main script finished loading'); if (savedButton) { savedButton.click(); console.log(`Restored saved tab: ${savedTab}`); diff --git a/webui/src/main/resources/application/tabs.js b/webui/src/main/resources/application/tabs.js index df077274..bda25a00 100644 --- a/webui/src/main/resources/application/tabs.js +++ b/webui/src/main/resources/application/tabs.js @@ -1,6 +1,7 @@ const observer = new MutationObserver(updateTabs); const observerOptions = {childList: true, subtree: true}; const tabCache = new Map(); +let isRestoringTabs = false; export function updateTabs() { const tabButtons = document.querySelectorAll('.tab-button'); @@ -10,14 +11,11 @@ export function updateTabs() { tabsContainers.add(tabsContainer); if (button.hasListener) return; button.hasListener = true; - // console.log(`Adding click event listener to tab button: ${button.getAttribute('data-for-tab')}, button element:`, button); button.addEventListener('click', (event) => { - // console.log(`Tab button clicked: ${button.getAttribute('data-for-tab')}, event:`, event); event.stopPropagation(); const forTab = button.getAttribute('data-for-tab'); - const tabsContainerId = button.closest('.tabs-container').id; - // console.log(`Tabs container ID: ${tabsContainerId}, button:`, button); - // console.log(`Saving selected tab to localStorage: selectedTab_${tabsContainerId} = ${forTab}, button:`, button); + let tabsContainerId = button.closest('.tabs-container').id; + if (button.classList.contains('active')) return; // Skip if already active try { localStorage.setItem(`selectedTab_${tabsContainerId}`, forTab); tabCache.set(tabsContainerId, forTab); // Update the cache @@ -25,68 +23,78 @@ export function updateTabs() { console.warn('Failed to save tab state to localStorage:', e); } let tabsParent = button.closest('.tabs-container'); - const tabButtons = tabsParent.querySelectorAll('.tab-button'); - tabButtons.forEach(btn => { - if (btn.closest('.tabs-container') === tabsParent) { - btn.classList.remove('active'); - } + const allTabButtons = tabsParent.querySelectorAll('.tab-button'); + allTabButtons.forEach(btn => { + btn.classList.remove('active'); }); button.classList.add('active'); - // console.log(`Active tab set to: ${forTab}, button:`, button); + console.log(`Active tab set to: ${forTab}, button:`, button); let selectedContent = null; const tabContents = tabsParent.querySelectorAll('.tab-content'); tabContents.forEach(content => { - if (content.closest('.tabs-container') !== tabsParent) return; if (content.getAttribute('data-tab') === forTab) { content.classList.add('active'); content.style.display = 'block'; // Ensure the content is displayed - // console.log(`Content displayed for tab: ${forTab}, content element:`, content); selectedContent = content; + // Recursively update nested tabs + updateNestedTabs(selectedContent); } else { content.classList.remove('active'); - content.style.display = 'none'; // Ensure the content is hidden - // console.log(`Content hidden for tab: ${content.getAttribute('data-tab')}, content element:`, content); + content.style.display = 'none'; // Hide the content instead of removing it } }); - if (selectedContent !== null) { - requestAnimationFrame(() => updateNestedTabs(selectedContent)); - } }); }); // Restore the selected tabs from localStorage - tabsContainers.forEach(tabsContainer => { - const savedTab = getSavedTab(tabsContainer.id); - if (savedTab) { - const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); - if (savedButton) { - savedButton.click(); // Simulate a click to activate the tab - // console.log(`Restored saved tab: ${savedTab}`); + isRestoringTabs = true; + try { + tabsContainers.forEach(tabsContainer => { + const savedTab = getSavedTab(tabsContainer.id); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + if (savedButton) { + savedButton.click(); // Activate the tab + } + } else { + tabsContainer.querySelector('.tab-button')?.click(); // Activate the first tab } - } else { - tabsContainer.querySelector('.tab-button')?.click(); // Activate the first tab - } - }); + }); + } finally { + isRestoringTabs = false; + } } export function restoreTabs() { + isRestoringTabs = true; // Restore the selected tabs from localStorage before adding event listeners - document.querySelectorAll('.tabs-container').forEach(tabsContainer => { - const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); - if (savedTab) { - const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); - if (savedButton) { - savedButton.classList.add('active'); - const forTab = savedButton.getAttribute('data-for-tab'); - const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); - if (selectedContent) { - selectedContent.classList.add('active'); - selectedContent.style.display = 'block'; + try { + document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + const savedTab = localStorage.getItem(`selectedTab_${tabsContainer.id}`); + if (savedTab) { + const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); + tabsContainer.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); + if (savedButton) { + savedButton.classList.add('active'); + const forTab = savedButton.getAttribute('data-for-tab'); + const selectedContent = tabsContainer.querySelector(`.tab-content[data-tab="${forTab}"]`); + if (selectedContent) { + selectedContent.classList.add('active'); + selectedContent.style.display = 'block'; // Ensure the content is displayed + updateNestedTabs(selectedContent); + } + // Hide other tab contents + tabsContainer.querySelectorAll(`.tab-content:not([data-tab="${forTab}"])`).forEach(content => { + content.style.display = 'none'; + }); + } else { + tabsContainer.querySelector('.tab-button')?.click(); // Activate the first tab if no saved tab } - console.log(`Restored saved tab: ${savedTab}`); } - } - }); + }); + } finally { + isRestoringTabs = false; + } } function getSavedTab(containerId) { @@ -103,36 +111,37 @@ function getSavedTab(containerId) { } } - function updateNestedTabs(element) { - const tabsContainers = element.querySelectorAll('.tabs-container'); - tabsContainers.forEach(tabsContainer => { - try { - let hasActiveButton = false; - const nestedButtons = tabsContainer.querySelectorAll('.tab-button'); - nestedButtons.forEach(nestedButton => { - // console.log(`Checking nested button: ${nestedButton.getAttribute('data-for-tab')}, nestedButton element:`, nestedButton); - if (nestedButton.classList.contains('active')) { - hasActiveButton = true; - } - }); - if (!hasActiveButton) { - const activeContent = tabsContainer.querySelector('.tab-content.active'); - if (activeContent) { - const activeTab = activeContent.getAttribute('data-tab'); - const activeButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${activeTab}"]`); - if (activeButton) { - activeButton.click(); // Simulate a click to activate the tab - } - } else { - tabsContainer.querySelector('.tab-button')?.click(); // Activate the first tab - } - } +function updateNestedTabs(element) { + const tabsContainers = element.querySelectorAll('.tabs-container'); + tabsContainers.forEach(tabsContainer => { + try { + tabsContainer.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); + let hasActiveButton = false; + const nestedButtons = tabsContainer.querySelectorAll('.tab-button'); + nestedButtons.forEach(nestedButton => { + if (nestedButton.classList.contains('active')) { + hasActiveButton = true; + } + }); + if (!hasActiveButton) { + const activeContent = tabsContainer.querySelector('.tab-content.active'); + if (activeContent) { + const activeTab = activeContent.getAttribute('data-tab'); + const activeButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${activeTab}"]`); + if (activeButton) { + setTimeout(() => activeButton.click(), 0); // Activate the tab asynchronously + } + } else { + setTimeout(() => tabsContainer.querySelector('.tab-button')?.click(), 0); // Activate the first tab asynchronously + } + } const savedTab = getSavedTab(tabsContainer.id); + console.log(`Saved tab for container ${tabsContainer.id}: ${savedTab}`); if (savedTab) { const savedButton = tabsContainer.querySelector(`.tab-button[data-for-tab="${savedTab}"]`); if (savedButton) { - if (!savedButton.classList.contains('active')) { - savedButton.click(); // Simulate a click to activate the tab only if it's not already active + if (!savedButton.classList.contains('active')) { + setTimeout(() => savedButton.click(), 0); // Activate the tab only if it's not already active, asynchronously } } } @@ -145,8 +154,8 @@ function getSavedTab(containerId) { document.addEventListener('DOMContentLoaded', () => { updateTabs(); observer.observe(document.body, observerOptions); - }); + window.addEventListener('beforeunload', () => { observer.disconnect(); }); diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt index 9623ca69..aeb8c614 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt @@ -2,6 +2,9 @@ package com.simiacryptus.skyenet.webui import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.util.ClientUtil.keyTxt +import com.simiacryptus.skyenet.apps.general.PlanAheadApp +import com.simiacryptus.skyenet.apps.plan.PlanCoordinator +import com.simiacryptus.skyenet.apps.plan.Settings import com.simiacryptus.skyenet.core.actors.CodingActor import com.simiacryptus.skyenet.core.actors.ImageActor import com.simiacryptus.skyenet.core.actors.ParsedActor @@ -73,7 +76,33 @@ object ActorTestAppServer : com.simiacryptus.skyenet.webui.application.Applicati "/test_coding_groovy", CodingActorTestApp(CodingActor(GroovyInterpreter::class, model = ChatModels.GPT35Turbo)) ), - ChildWebApp("/test_file_patch", FilePatchTestApp()) + ChildWebApp("/test_file_patch", FilePatchTestApp()), + /*PlanAheadApp*/ + ChildWebApp( + "/taskDev", + PlanAheadApp( + rootFile = null, + settings = Settings( + model = ChatModels.GPT4o, + parsingModel = ChatModels.GPT4oMini, + command = listOf("task"), + temperature = 0.2, + budget = 2.0, + taskPlanningEnabled = true, + shellCommandTaskEnabled = false, + autoFix = true, + enableCommandAutoFix = true, + commandAutoFixCommands = listOf( + "C:\\Program Files\\nodejs\\npx.cmd", "C:\\Program Files\\nodejs\\npm.cmd" + ), + env = mapOf(), + workingDir = ".", + language = if (PlanCoordinator.isWindows) "powershell" else "bash", + ), + model = ChatModels.GPT4o, + parsingModel = ChatModels.GPT4oMini, + ) + ), ) }