From 68fc3756084f30fd5a5c64200abd55b1b4fe7c11 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 15 Sep 2024 19:58:30 -0400 Subject: [PATCH] 1.2.3 (#101) * 1.2.3 * wip * Update ActorTestAppServer.kt * wip * 1.2.3 * wip * wip * wip * Update uiHandlers.js --- README.md | 6 +- core/build.gradle.kts | 2 +- core/src/main/dev_documentation.md | 12 +- .../skyenet/core/actors/CodingActor.kt | 3 +- .../skyenet/core/actors/ParsedActor.kt | 11 +- .../skyenet/core/platform/file/DataStorage.kt | 2 +- .../skyenet/core/platform/test/UsageTest.kt | 13 +- .../simiacryptus/skyenet/core/util/Ears.kt | 3 +- docs/core_user_documentation.md | 22 +- gradle.properties | 2 +- webui/build.gradle.kts | 2 +- webui/src/compiled_documentation.md | 6 +- .../diff/AddApplyFileDiffLinks.kt | 22 +- .../simiacryptus/diff/IterativePatchUtil.kt | 59 +- .../simiacryptus/diff/IterativePatchUtil.md | 279 +++++++ .../com/simiacryptus/skyenet/TabbedDisplay.kt | 2 +- .../skyenet/apps/general/DocumentParserApp.kt | 3 +- .../skyenet/apps/general/PlanAheadApp.kt | 8 +- .../skyenet/apps/general/PlanChatApp.kt | 17 +- .../skyenet/apps/general/WebDevApp.kt | 5 +- .../general/parsers/DefaultParsingModel.kt | 4 +- .../skyenet/apps/plan/AbstractAnalysisTask.kt | 4 +- .../skyenet/apps/plan/AbstractTask.kt | 10 +- .../skyenet/apps/plan/CommandAutoFixTask.kt | 121 ++- .../skyenet/apps/plan/DocumentationTask.kt | 14 +- .../skyenet/apps/plan/FileModificationTask.kt | 78 +- .../skyenet/apps/plan/ForeachTask.kt | 7 +- .../skyenet/apps/plan/InquiryTask.kt | 36 +- .../skyenet/apps/plan/PlanCoordinator.kt | 91 ++- .../skyenet/apps/plan/PlanProcessingState.kt | 2 +- .../skyenet/apps/plan/PlanSettings.kt | 116 ++- .../skyenet/apps/plan/PlanUtil.kt | 90 ++- .../skyenet/apps/plan/PlanningTask.kt | 58 +- .../skyenet/apps/plan/RunShellCommandTask.kt | 32 +- .../skyenet/apps/plan/TaskType.kt | 67 +- .../skyenet/apps/plan/planning_agent.md | 759 ++++++++++++++++++ .../webui/session/SocketManagerBase.kt | 6 +- .../main/resources/application/functions.js | 54 +- webui/src/main/resources/application/main.js | 24 +- webui/src/main/resources/application/tabs.js | 50 +- .../main/resources/application/uiHandlers.js | 38 +- .../src/main/resources/application/uiSetup.js | 32 +- .../skyenet/webui/ActorTestAppServer.kt | 34 +- webui/src/test/resources/logback.xml | 2 +- 44 files changed, 1687 insertions(+), 521 deletions(-) create mode 100644 webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.md create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/planning_agent.md diff --git a/README.md b/README.md index c7725f5b..1f34edd1 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,18 @@ Maven: com.simiacryptus skyenet-webui - 1.1.2 + 1.1.4 ``` Gradle: ```groovy -implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.1.2' +implementation group: 'com.simiacryptus', name: 'skyenet', version: '1.1.4' ``` ```kotlin -implementation("com.simiacryptus:skyenet:1.1.2") +implementation("com.simiacryptus:skyenet:1.1.4") ``` ### 🌟 To Use diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8f0afd39..96c43774 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.1.2") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.1.4") implementation(group = "org.hsqldb", name = "hsqldb", version = hsqldb_version) implementation("org.apache.commons:commons-text:1.11.0") diff --git a/core/src/main/dev_documentation.md b/core/src/main/dev_documentation.md index 4041a588..5845041b 100644 --- a/core/src/main/dev_documentation.md +++ b/core/src/main/dev_documentation.md @@ -444,7 +444,7 @@ sequenceDiagram ```kotlin val imageActor = ImageActor( prompt = "Transform the user request into an image generation prompt that the user will like", - textModel = ChatModels.GPT3_5_Turbo, + textModel = OpenAIModels.GPT3_5_Turbo, imageModel = ImageModels.DallE2, temperature = 0.3, width = 1024, @@ -757,8 +757,8 @@ Creates a new instance of `ParsedActor` with the specified OpenAI model. val actor = ParsedActor( resultClass = MyClass::class.java, prompt = "Please parse the following message:", - model = ChatModels.GPT_3_5_Turbo, - parsingModel = ChatModels.GPT_3_5_Turbo + model = OpenAIModels.GPT_3_5_Turbo, + parsingModel = OpenAIModels.GPT_3_5_Turbo ) val api = OpenAIClient("your_api_key") @@ -1388,7 +1388,7 @@ Intercepts calls to the `render` method of the `TextToSpeechActor`. It allows fo ```kotlin // Create an instance of TextToSpeechActor -val originalActor = TextToSpeechActor("actorName", audioModel, "alloy", 1.0, ChatModels.GPT35Turbo) +val originalActor = TextToSpeechActor("actorName", audioModel, "alloy", 1.0, OpenAIModels.GPT35Turbo) // Define a function wrapper to intercept calls val interceptor = FunctionWrapper() @@ -1452,7 +1452,7 @@ classDiagram #### Methods -- `override fun actorFactory(prompt: String): CodingActor`: Creates an instance of `CodingActor` with the specified `interpreterClass`, `details` (prompt), and the model set to `ChatModels.GPT35Turbo`. +- `override fun actorFactory(prompt: String): CodingActor`: Creates an instance of `CodingActor` with the specified `interpreterClass`, `details` (prompt), and the model set to `OpenAIModels.GPT35Turbo`. - `override fun getPrompt(actor: BaseActor): String`: Retrieves the prompt details from the given `CodingActor` instance. - `override fun resultMapper(result: CodeResult): String`: Maps the `CodeResult` to its `code` property, effectively extracting the generated code snippet. @@ -2761,7 +2761,7 @@ This test method validates the functionality of the `incrementUsage` method with 1. **Setup**: A test user is created with predefined attributes. A session ID is generated using `StorageInterface.newGlobalID()`. A predefined usage object is created to simulate the consumption of resources. -2. **Action**: The `incrementUsage` method of the `UsageInterface` implementation is called with the session ID, test user, a model (in this case, `ChatModels.GPT35Turbo`), and the predefined usage object. +2. **Action**: The `incrementUsage` method of the `UsageInterface` implementation is called with the session ID, test user, a model (in this case, `OpenAIModels.GPT35Turbo`), and the predefined usage object. 3. **Verification**: - The method `getSessionUsageSummary` is called with the session ID to retrieve the usage summary for the session. The test verifies that the returned usage summary matches the predefined usage object. 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 aa497f84..e9099a2f 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 @@ -7,6 +7,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.OpenAIModels import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.skyenet.core.OutputInterceptor @@ -25,7 +26,7 @@ open class CodingActor( name: String? = interpreterClass.simpleName, val details: String? = null, model: OpenAITextModel, - val fallbackModel: ChatModels = ChatModels.GPT4o, + val fallbackModel: ChatModels = OpenAIModels.GPT4o, temperature: Double = 0.1, val runtimeSymbols: Map = mapOf() ) : BaseActor( 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 e19fc20d..602319f8 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 @@ -7,6 +7,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.OpenAIModels import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.jopenai.util.JsonUtil @@ -17,9 +18,9 @@ open class ParsedActor( val exampleInstance: T? = resultClass?.getConstructor()?.newInstance(), prompt: String = "", name: String? = resultClass?.simpleName, - model: OpenAITextModel = ChatModels.GPT4o, + model: OpenAITextModel = OpenAIModels.GPT4o, temperature: Double = 0.3, - val parsingModel: OpenAITextModel = ChatModels.GPT35Turbo, + val parsingModel: OpenAITextModel = OpenAIModels.GPT4oMini, val deserializerRetries: Int = 2, open val describer: TypeDescriber = object : AbbrevWhitelistYamlDescriber( "com.simiacryptus", "com.github.simiacryptus" @@ -85,9 +86,9 @@ open class ParsedActor( ApiModel.ChatMessage(role = ApiModel.Role.user, content = "The user message to parse:\n\n$input".toContentList()), ), temperature = temperature, - model = model.modelName, + model = parsingModel.modelName, ), - model = model, + model = parsingModel, ).choices.first().message?.content var contentUnwrapped = content?.trim() ?: throw RuntimeException("No response") @@ -144,4 +145,4 @@ open class ParsedActor( companion object { private val log = org.slf4j.LoggerFactory.getLogger(ParsedActor::class.java) } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt index 8f292836..f9f9960f 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/file/DataStorage.kt @@ -16,7 +16,7 @@ open class DataStorage( ) : StorageInterface { init { - log.info("Data directory: ${dataDir.absolutePath}", RuntimeException()) + log.debug("Data directory: ${dataDir.absolutePath}", RuntimeException()) } override fun getMessages( diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/UsageTest.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/UsageTest.kt index 83e1ef10..3b31304a 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/UsageTest.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/platform/test/UsageTest.kt @@ -2,6 +2,7 @@ package com.simiacryptus.skyenet.core.platform.test import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAIModels import com.simiacryptus.skyenet.core.platform.StorageInterface import com.simiacryptus.skyenet.core.platform.UsageInterface import com.simiacryptus.skyenet.core.platform.User @@ -24,7 +25,7 @@ abstract class UsageTest(private val impl: UsageInterface) { @Test fun `incrementUsage should increment usage for session`() { - val model = ChatModels.GPT35Turbo + val model = OpenAIModels.GPT4oMini val session = StorageInterface.newGlobalID() val usage = ApiModel.Usage( prompt_tokens = 10, @@ -40,7 +41,7 @@ abstract class UsageTest(private val impl: UsageInterface) { @Test fun `getUserUsageSummary should return correct usage summary`() { - val model = ChatModels.GPT35Turbo + val model = OpenAIModels.GPT4oMini val session = StorageInterface.newGlobalID() val usage = ApiModel.Usage( prompt_tokens = 15, @@ -54,7 +55,7 @@ abstract class UsageTest(private val impl: UsageInterface) { @Test fun `clear should reset all usage data`() { - val model = ChatModels.GPT35Turbo + val model = OpenAIModels.GPT4oMini val session = StorageInterface.newGlobalID() val usage = ApiModel.Usage( prompt_tokens = 20, @@ -71,8 +72,8 @@ abstract class UsageTest(private val impl: UsageInterface) { @Test fun `incrementUsage should handle multiple models correctly`() { - val model1 = ChatModels.GPT35Turbo - val model2 = ChatModels.GPT4Turbo + val model1 = OpenAIModels.GPT4oMini + val model2 = OpenAIModels.GPT4Turbo val session = StorageInterface.newGlobalID() val usage1 = ApiModel.Usage( prompt_tokens = 10, @@ -96,7 +97,7 @@ abstract class UsageTest(private val impl: UsageInterface) { @Test fun `incrementUsage should accumulate usage for the same model`() { - val model = ChatModels.GPT35Turbo + val model = OpenAIModels.GPT4oMini val session = StorageInterface.newGlobalID() val usage1 = ApiModel.Usage( prompt_tokens = 10, diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/Ears.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/Ears.kt index b156b00b..7ff6b551 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/Ears.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/Ears.kt @@ -6,6 +6,7 @@ import com.simiacryptus.jopenai.audio.AudioRecorder import com.simiacryptus.jopenai.audio.LookbackLoudnessWindowBuffer import com.simiacryptus.jopenai.audio.TranscriptionProcessor import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAIModels import com.simiacryptus.jopenai.proxy.ChatProxy import org.slf4j.LoggerFactory import java.util.* @@ -38,7 +39,7 @@ open class Ears( open val commandRecognizer = ChatProxy( clazz = CommandRecognizer::class.java, api = api, - model = ChatModels.GPT35Turbo, + model = OpenAIModels.GPT4oMini, ).create() open fun timeout(ms: Long): () -> Boolean { diff --git a/docs/core_user_documentation.md b/docs/core_user_documentation.md index 749d00b5..2a735c38 100644 --- a/docs/core_user_documentation.md +++ b/docs/core_user_documentation.md @@ -255,7 +255,7 @@ The `BaseActor` class abstracts the process of sending prompts to an OpenAI mode - `prompt`: The initial prompt or question to be sent to the model. - `name`: An optional name for the actor. Useful for identification purposes in more complex scenarios. -- `model`: The OpenAI model to be used. Defaults to `ChatModels.GPT35Turbo`. +- `model`: The OpenAI model to be used. Defaults to `OpenAIModels.GPT35Turbo`. - `temperature`: Controls the randomness of the model's responses. Lower values make responses more deterministic. @@ -349,8 +349,8 @@ To create an instance of `CodingActor`, you need to provide the following parame - `describer`: An instance of `TypeDescriber` used to describe the types of the provided symbols. - `name`: An optional name for the actor. - `details`: Optional additional details to be included in the prompt sent to the OpenAI API. -- `model`: The OpenAI model to be used for code generation (default is `ChatModels.GPT35Turbo`). -- `fallbackModel`: A fallback OpenAI model to be used in case of failure with the primary model (default is `ChatModels.GPT4o`). +- `model`: The OpenAI model to be used for code generation (default is `OpenAIModels.GPT35Turbo`). +- `fallbackModel`: A fallback OpenAI model to be used in case of failure with the primary model (default is `OpenAIModels.GPT4o`). - `temperature`: The temperature parameter for the OpenAI API requests (default is `0.1`). - `runtimeSymbols`: Additional symbols to be added at runtime. @@ -362,7 +362,7 @@ val codingActor = CodingActor( symbols = mapOf("exampleSymbol" to Any()), name = "MyCodingActor", details = "This is a detailed description of what the actor does.", - model = ChatModels.GPT35Turbo + model = OpenAIModels.GPT35Turbo ) ``` @@ -435,7 +435,7 @@ To create an instance of `ImageActor`, you can use the following constructor: val imageActor = ImageActor( prompt = "Transform the user request into an image generation prompt that the user will like", name = null, - textModel = ChatModels.GPT35Turbo, + textModel = OpenAIModels.GPT35Turbo, imageModel = ImageModels.DallE2, temperature = 0.3, width = 1024, @@ -474,7 +474,7 @@ You can customize the `ImageActor` by changing its model settings: - To change the chat model: ```kotlin -val newChatModel: ChatModels = ChatModels.GPT4 +val newChatModel: ChatModels = OpenAIModels.GPT4 val updatedActor = imageActor.withModel(newChatModel) ``` @@ -649,9 +649,9 @@ The `ParsedActor` class is a specialized actor designed to parse responses from - `parserClass`: The class of the parser function used to convert chat model responses into the desired data type. - `prompt`: The initial prompt to send to the chat model. - `name`: An optional name for the actor. Defaults to the simple name of the parser class if not provided. -- `model`: The chat model to use for generating responses. Defaults to `ChatModels.GPT35Turbo`. +- `model`: The chat model to use for generating responses. Defaults to `OpenAIModels.GPT35Turbo`. - `temperature`: The temperature setting for the chat model, affecting the randomness of responses. Defaults to `0.3`. -- `parsingModel`: The chat model to use specifically for parsing responses. Defaults to `ChatModels.GPT35Turbo`. +- `parsingModel`: The chat model to use specifically for parsing responses. Defaults to `OpenAIModels.GPT35Turbo`. - `deserializerRetries`: The number of retries for deserialization in case of parsing errors. Defaults to `2`. @@ -1349,7 +1349,7 @@ The `SimpleActor` class is part of the `com.simiacryptus.skyenet.core.actors` pa - `prompt`: A `String` representing the initial prompt or context to be sent to the model. - `name`: An optional `String` parameter that specifies the name of the actor. It defaults to `null` if not provided. -- `model`: Specifies the model to be used for generating responses. It defaults to `ChatModels.GPT35Turbo`. +- `model`: Specifies the model to be used for generating responses. It defaults to `OpenAIModels.GPT35Turbo`. - `temperature`: A `Double` value that controls the randomness of the model's responses. Lower values make the model more deterministic. It defaults to `0.3`. @@ -1405,7 +1405,7 @@ Creates a new instance of `SimpleActor` with the specified model while retaining val simpleActor = SimpleActor( prompt = "Hello, how can I assist you today?", name = "Assistant", - model = ChatModels.GPT35Turbo, + model = OpenAIModels.GPT35Turbo, temperature = 0.3 ) @@ -1530,7 +1530,7 @@ The `ParsedActorTestBase` class is designed to streamline the process of testing override fun actorFactory(prompt: String): ParsedActor ``` -- **Description**: Creates an instance of `ParsedActor` with the specified prompt and parser class. The parsing model is set to `ChatModels.GPT35Turbo` by default. +- **Description**: Creates an instance of `ParsedActor` with the specified prompt and parser class. The parsing model is set to `OpenAIModels.GPT35Turbo` by default. - **Parameters**: - `prompt`: A string representing the prompt to be used by the actor. - **Returns**: An instance of `ParsedActor` configured with the provided prompt and parser. diff --git a/gradle.properties b/gradle.properties index 7f39cce4..f0b5132b 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.2.2 +libraryVersion = 1.2.3 gradleVersion = 7.6.1 kotlin.daemon.jvmargs=-Xmx2g diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts index 9e6429ed..2503434e 100644 --- a/webui/build.gradle.kts +++ b/webui/build.gradle.kts @@ -36,7 +36,7 @@ val jackson_version = "2.17.2" dependencies { - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.1.2") { + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.1.4") { exclude(group = "org.slf4j") } diff --git a/webui/src/compiled_documentation.md b/webui/src/compiled_documentation.md index 1dc186b4..9168fe6b 100644 --- a/webui/src/compiled_documentation.md +++ b/webui/src/compiled_documentation.md @@ -219,7 +219,7 @@ val codingAgent = CodingAgent( symbols = mapOf("exampleSymbol" to Any()), temperature = 0.1, details = "Optional details", - model = ChatModels.GPT35Turbo + model = OpenAIModels.GPT35Turbo ) // Start the code generation process with a user message @@ -279,7 +279,7 @@ interpreter: KClass, symbols: Map, temperature: Double = 0.1, details: String?, -model: ChatModels = ChatModels.GPT35Turbo, +model: ChatModels = OpenAIModels.GPT35Turbo, actorMap: Map ) ``` @@ -1041,7 +1041,7 @@ sending and receiving messages, processing user inputs, and generating responses ### Constructor Parameters - `session`: The current user session. -- `model`: The OpenAI text model to use for generating responses. Default is `ChatModels.GPT35Turbo`. +- `model`: The OpenAI text model to use for generating responses. Default is `OpenAIModels.GPT35Turbo`. - `userInterfacePrompt`: A prompt displayed to the user at the start of the chat session. - `initialAssistantPrompt`: The initial message from the assistant. Default is an empty string. - `systemPrompt`: A system-level prompt that influences the assistant's responses. diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt b/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt index 18b6a7dd..217e2937 100644 --- a/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt +++ b/webui/src/main/kotlin/com/simiacryptus/diff/AddApplyFileDiffLinks.kt @@ -3,7 +3,7 @@ package com.simiacryptus.diff import com.simiacryptus.diff.FileValidationUtils.Companion.isGitignore import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.OpenAIClient -import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAIModels import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs import com.simiacryptus.skyenet.core.actors.SimpleActor import com.simiacryptus.skyenet.set @@ -100,7 +100,7 @@ private fun SocketManagerBase.renderNewFile( |${codeValue} |``` | - |
Automatically Saved ${filename}
+ |
Automatically Saved ${filepath}
|""".trimMargin() } catch (e: Throwable) { return """ @@ -119,7 +119,7 @@ private fun SocketManagerBase.renderNewFile( filepath.parent?.toFile()?.mkdirs() filepath.toFile().writeText(codeValue, Charsets.UTF_8) handle(mapOf(File(filename).toPath() to codeValue)) - hrefLink.set("""
Saved ${filename}
""") + hrefLink.set("""
Saved ${filepath}
""") commandTask.complete() } catch (e: Throwable) { hrefLink.append("""
Error: ${e.message}
""") @@ -132,7 +132,7 @@ private fun SocketManagerBase.renderNewFile( |``` | |${commandTask.placeholder} - |""".trimMargin() + """.trimMargin() } } @@ -150,7 +150,7 @@ fun resolve(root: Path, filename: String): String { filename = try { val path = File(filename).toPath() - if (root.contains(path)) root.relativize(path).toString() else filename + if (root.contains(path)) path.toString().relativizeFrom(root) else filename } catch (e: Throwable) { filename } @@ -158,16 +158,22 @@ fun resolve(root: Path, filename: String): String { if (!root.resolve(filename).toFile().exists()) { root.toFile().listFilesRecursively().find { it.toString().replace("\\", "/").endsWith(filename.replace("\\", "/")) } ?.toString()?.apply { - filename = root.relativize(File(this).toPath()).toString() + filename = relativizeFrom(root) } } return filename } +private fun String.relativizeFrom(root: Path) = try { + root.relativize(File(this).toPath()).toString() +} catch (e: Throwable) { + this +} + private fun File.listFilesRecursively(): List { val files = mutableListOf() - this.listFiles().filter { !isGitignore(it.toPath()) }.forEach { + this.listFiles()?.filter { !isGitignore(it.toPath()) }?.forEach { files.add(it.absoluteFile) if (it.isDirectory) { files.addAll(it.listFilesRecursively()) @@ -320,7 +326,7 @@ private fun SocketManagerBase.renderDiffBlock( | }); |``` """.trimMargin(), - model = ChatModels.GPT4o, + model = OpenAIModels.GPT4o, temperature = 0.3 ) diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt index 9535b218..459c6acd 100644 --- a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.kt @@ -43,25 +43,19 @@ object IterativePatchUtil { return sb.toString() } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as LineRecord - if (index != other.index) return false if (line != other.line) return false - if (type != other.type) return false - if (metrics != other.metrics) return false - return true } override fun hashCode(): Int { var result = index result = 31 * result + (line?.hashCode() ?: 0) - result = 31 * result + type.hashCode() - result = 31 * result + metrics.hashCode() return result } @@ -147,6 +141,8 @@ object IterativePatchUtil { log.debug("Starting to mark moved lines") // We start with the first line of the new (patched) code var newLine = newLines.firstOrNull() + var iterationCount = 0 + val maxIterations = newLines.size * 2 // Arbitrary limit to prevent infinite loops // We'll iterate through all lines of the new code while (null != newLine) { try { @@ -169,6 +165,11 @@ object IterativePatchUtil { // Skip any lines in the source that don't have a match or are deletions // This helps us find the next "anchor" point in the source code while (nextSourceLine.matchingLine == null || nextSourceLine.type == DELETE) { + // Safeguard to prevent infinite loop + if (++iterationCount > maxIterations) { + log.error("Exceeded maximum iterations in markMovedLines") + break + } nextSourceLine = nextSourceLine.nextLine ?: break } if(nextSourceLine.matchingLine == null || nextSourceLine.type == DELETE) break @@ -190,8 +191,13 @@ object IterativePatchUtil { } } } finally { - // Move to the next line to process in the outer loop - newLine = nextNewLine + // Safeguard to prevent infinite loop + if (++iterationCount > maxIterations) { + log.error("Exceeded maximum iterations in markMovedLines") + newLine = nextNewLine + // Move to the next line to process in the outer loop + // newLine = nextNewLine + } } } else { // If the current line doesn't have a match, move to the next one @@ -287,8 +293,9 @@ object IterativePatchUtil { * @return The normalized line. */ private fun normalizeLine(line: String): String { - return line.replace("\\s".toRegex(), "") + return line.replace(whitespaceRegex, "") } + private val whitespaceRegex = "\\s".toRegex() private fun link( sourceLines: List, @@ -697,26 +704,26 @@ private fun generatePatchedText( */ private fun calculateLineMetrics(lines: List) { log.debug("Starting to calculate line metrics for ${lines.size} lines") - var parenthesesDepth = 0 - var squareBracketsDepth = 0 - var curlyBracesDepth = 0 - - lines.forEach { lineRecord -> - lineRecord.line?.forEach { char -> + lines.fold( + Triple(0, 0, 0) + ) { (parenDepth, squareDepth, curlyDepth), lineRecord -> + val updatedDepth = lineRecord.line?.fold(Triple(parenDepth, squareDepth, curlyDepth)) { acc, char -> when (char) { - '(' -> parenthesesDepth++ - ')' -> parenthesesDepth = maxOf(0, parenthesesDepth - 1) - '[' -> squareBracketsDepth++ - ']' -> squareBracketsDepth = maxOf(0, squareBracketsDepth - 1) - '{' -> curlyBracesDepth++ - '}' -> curlyBracesDepth = maxOf(0, curlyBracesDepth - 1) + '(' -> Triple(acc.first + 1, acc.second, acc.third) + ')' -> Triple(max(0, acc.first - 1), acc.second, acc.third) + '[' -> Triple(acc.first, acc.second + 1, acc.third) + ']' -> Triple(acc.first, max(0, acc.second - 1), acc.third) + '{' -> Triple(acc.first, acc.second, acc.third + 1) + '}' -> Triple(acc.first, acc.second, max(0, acc.third - 1)) + else -> acc } - } + } ?: Triple(parenDepth, squareDepth, curlyDepth) lineRecord.metrics = LineMetrics( - parenthesesDepth = parenthesesDepth, - squareBracketsDepth = squareBracketsDepth, - curlyBracesDepth = curlyBracesDepth + parenthesesDepth = updatedDepth.first, + squareBracketsDepth = updatedDepth.second, + curlyBracesDepth = updatedDepth.third ) + updatedDepth } log.debug("Finished calculating line metrics") } diff --git a/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.md b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.md new file mode 100644 index 00000000..2c8c8739 --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/diff/IterativePatchUtil.md @@ -0,0 +1,279 @@ +The `IterativePatchUtil` Kotlin object implements a sophisticated algorithm for generating and applying patches between two versions of textual content, such as source code files. This algorithm is designed to identify differences, categorize them, and efficiently apply those differences to produce the updated text. Below is a comprehensive breakdown of how this patching algorithm operates, including its key components and step-by-step processes. + +## **1. Overview** + +The `IterativePatchUtil` serves two primary functions: + +1. **Generating a Patch**: Comparing an original ("old") version of text with a modified ("new") version to produce a patch that encapsulates the differences. +2. **Applying a Patch**: Taking an original text and a patch to produce the updated text by applying the changes encapsulated in the patch. + +The algorithm emphasizes maintaining the structural integrity of the text, especially regarding bracket nesting (parentheses, square brackets, curly braces), and optimizes the patch by minimizing unnecessary changes. + +## **2. Core Components** + +### **a. Data Structures** + +- **`LineType` Enum**: Categorizes each line as one of three types: + - `CONTEXT`: Lines that are unchanged between versions. + - `ADD`: Lines that have been added in the new version. + - `DELETE`: Lines that have been removed from the old version. + +- **`LineMetrics` Data Class**: Tracks the nesting depth of different types of brackets within a line: + - `parenthesesDepth`: Depth of `()` brackets. + - `squareBracketsDepth`: Depth of `[]` brackets. + - `curlyBracesDepth`: Depth of `{}` brackets. + +- **`LineRecord` Data Class**: Represents a single line in either the source or patch text, containing: + - `index`: Line number. + - `line`: The actual text of the line. + - `previousLine` & `nextLine`: Pointers to adjacent lines. + - `matchingLine`: Links to the corresponding `LineRecord` in the other version. + - `type`: Categorization (`CONTEXT`, `ADD`, `DELETE`). + - `metrics`: Bracket nesting metrics for the line. + +### **b. Logging** + +Utilizes `SLF4J` (`LoggerFactory`) for logging various stages and actions within the algorithm, aiding in debugging and tracking the patching process. + +## **3. Patch Generation Process (`generatePatch`)** + +The `generatePatch` function orchestrates the creation of a patch by comparing the old and new versions of the text. Here's a detailed step-by-step breakdown: + +### **Step 1: Parsing the Texts** + +- **`parseLines` Function**: + - Splits the input text into individual lines. + - Creates `LineRecord` instances for each line. + - Establishes `previousLine` and `nextLine` links between consecutive lines. + - Calculates bracket metrics (`parenthesesDepth`, `squareBracketsDepth`, `curlyBracesDepth`) for each line using the `calculateLineMetrics` function. + +### **Step 2: Linking Lines Between Source and New Texts** + +- **`link` Function**: + - **Step 1: Linking Unique Exact Matches** (`linkUniqueMatchingLines`): + - Groups lines from both source and new texts based on their normalized content (whitespace removed). + - Identifies lines that appear uniquely and match exactly between both versions. + - Links such lines by setting their `matchingLine` references. + + - **Step 2: Linking Adjacent Exact Matches** (`linkAdjacentMatchingLines`): + - Iteratively scans for lines adjacent to already linked lines that match exactly. + - Links these adjacent lines to extend the set of matching lines. + + - **Step 3: Subsequence Linking** (`subsequenceLinking`): + - Recursively attempts to link remaining unmatched lines, potentially using Levenshtein distance for lines that are similar but not identical. + - This step ensures that even moved blocks or slightly altered lines are recognized and correctly linked. + +### **Step 3: Marking Moved Lines** + +- **`markMovedLines` Function**: + - Iterates through the new lines to identify lines that have been moved rather than added or deleted. + - Detects discrepancies in the order of linked lines between source and new texts. + - Marks such lines appropriately by changing their `LineType` to `ADD` or `DELETE` to reflect the movement. + +### **Step 4: Generating the Diff** + +- **`newToPatch` Function**: + - Traverses through the new lines to construct a preliminary list of differences (`diff`), categorizing each as `ADD`, `DELETE`, or `CONTEXT`. + - For lines with no matching counterpart in the source, marks them as `ADD`. + - For matched lines, checks surrounding lines to identify any `DELETE` operations required. + +### **Step 5: Truncating Context** + +- **`truncateContext` Function**: + - Limits the number of unchanged (`CONTEXT`) lines around changes to reduce patch size and improve readability. + - Uses a `contextSize` parameter (set to 3) to determine how many context lines to retain before and after changes. + - If there are more context lines than the specified `contextSize`, replaces the excess with an ellipsis (`...`). + +### **Step 6: Fixing Patch Line Order** + +- **`fixPatchLineOrder` Function**: + - Ensures that `DELETE` lines precede `ADD` lines when they are adjacent in the diff. + - Iteratively swaps lines in the diff to maintain this order, which is essential for properly applying the patch. + +### **Step 7: Annihilating No-Op Line Pairs** + +- **`annihilateNoopLinePairs` Function**: + - Removes pairs of lines where a line is deleted and then immediately added without any actual change in content. + - Helps in cleaning up the diff by eliminating unnecessary operations that cancel each other out. + +### **Step 8: Generating the Final Patch Text** + +- Iterates through the processed `shortDiff` list to build the final patch string. +- Formats each line based on its `LineType`: + - `CONTEXT`: Prefixes the line with two spaces (` `). + - `ADD`: Prefixes the line with a plus sign (`+`). + - `DELETE`: Prefixes the line with a minus sign (`-`). +- Joins all lines into a single string, trimming any trailing whitespace. + +## **4. Patch Application Process (`applyPatch`)** + +The `applyPatch` function applies a previously generated patch to an original text to produce the updated text. Here's how it works: + +### **Step 1: Parsing the Source and Patch Texts** + +- **Parsing Source Text**: + - Uses `parseLines` to parse the original text into `LineRecord` instances, similar to the patch generation process. + +- **Parsing Patch Text**: + - Uses `parsePatchLines` to parse the patch. + - **`parsePatchLines` Function**: + - Splits the patch into lines. + - Identifies the type of each line (`ADD`, `DELETE`, `CONTEXT`) based on prefixes (`+`, `-`, or none). + - Filters out metadata lines (e.g., lines starting with `+++`, `---`, `@@`). + - Establishes `previousLine` and `nextLine` links. + - Calculates bracket metrics. + +### **Step 2: Linking Source and Patch Lines** + +- **`link` Function**: + - Links lines from the source and patch texts using the same linking steps as in the patch generation process. + - In this context, it uses the `LevenshteinDistance` algorithm to accommodate minor differences between lines, aiding in more accurate linking. + +### **Step 3: Filtering Patch Lines** + +- Removes any lines in the patch that consist solely of whitespace to prevent unnecessary alterations during patch application. + +### **Step 4: Generating the Patched Text** + +- **`generatePatchedText` Function**: + - Iterates through the source lines and applies changes based on the linked patch lines. + - Handles different scenarios: + - **Deletion**: Skips lines marked as `DELETE`. + - **Addition**: Inserts lines marked as `ADD`. + - **Context**: Retains lines that haven't changed. + - Ensures that inserted lines are placed correctly by checking for inserts before and after matched lines. + - Reconstructs the updated text by compiling the processed lines into a final string. + +## **5. Supporting Functions and Utilities** + +### **a. Normalization** + +- **`normalizeLine` Function**: + - Removes all whitespace characters from a line to facilitate accurate comparison between lines. + - Helps in identifying lines that are functionally the same but may differ in formatting. + +### **b. Line Linking and Matching** + +- **`linkUniqueMatchingLines` Function**: + - Links lines that are uniquely identical between source and patch texts. + - Prevents multiple matches by ensuring that only lines with the same count in both texts are linked. + +- **`linkAdjacentMatchingLines` Function**: + - Extends existing links by connecting adjacent lines that match, enhancing the continuity of matched blocks. + +- **`isMatch` Function**: + - Determines if two lines match exactly or are similar based on the Levenshtein distance. + - Allows for minor discrepancies, improving the algorithm's robustness. + +### **c. Bracket Metrics Calculation** + +- **`calculateLineMetrics` Function**: + - Analyzes each line to determine the nesting depth of different brackets. + - Useful for understanding the structural context of code or text, aiding in more intelligent patching decisions. + +- **`lineMetrics` Extension Function**: + - Provides a convenient way to calculate bracket metrics for individual strings. + +## **6. Handling Edge Cases and Optimizations** + +- **Recursion Depth Control**: + - The `subsequenceLinking` function limits recursion depth to prevent excessive processing (`depth > 10`), ensuring the algorithm remains efficient. + +- **Preventing Infinite Loops**: + - Utilizes `require` statements to ensure that lines are not inadvertently linked to themselves, safeguarding against potential infinite loops during linking. + +- **Context Truncation**: + - By limiting the number of context lines, the algorithm reduces patch size and focuses on relevant changes, enhancing readability and manageability. + +- **No-Op Pair Removal**: + - Eliminates redundant add-delete pairs, streamlining the patch and avoiding unnecessary modifications. + +## **7. Logging and Debugging** + +Throughout the algorithm, extensive logging is implemented to track the progress and internal state at various stages: + +- **Informational Logs** (`log.info`): Indicate the commencement and completion of major processes like patch generation and application. + +- **Debug Logs** (`log.debug`): Provide detailed insights into intermediate steps, such as the number of lines parsed, linked, matched, and any modifications made during processing. + +- **Error Logs** (`log.error`): Capture and report any exceptions or critical issues encountered during execution, aiding in troubleshooting. + +## **8. Summary of Workflow** + +1. **Parsing**: Both the original and new texts are parsed into line-by-line `LineRecord` objects with bracket metrics calculated. +2. **Linking**: The algorithm attempts to link corresponding lines between the original and new texts using exact matches, adjacency, and subsequence strategies, incorporating Levenshtein distance for similar lines. +3. **Marking Movements**: Detects and marks lines that have been moved rather than added or deleted. +4. **Generating Diff**: Creates a preliminary diff list categorizing each line as `ADD`, `DELETE`, or `CONTEXT`. +5. **Optimizing Patch**: Truncates excess context, fixes the order of add/delete operations, and removes no-op pairs to refine the patch. +6. **Finalizing Patch**: Constructs the final patch string by formatting each line based on its type. +7. **Applying Patch**: Repeats parsing and linking steps to apply the patch, reconstructing the updated text by integrating additions, deletions, and retaining unchanged lines. + +## **9. Example Scenario** + +Consider two versions of a simple source code file: + +### **Original (`oldCode`)** + +```kotlin +fun add(a: Int, b: Int): Int { + return a + b +} +``` + +
+ + +### **Modified (`newCode`)** + +```kotlin +fun addNumbers(a: Int, b: Int): Int { + val sum = a + b + return sum +} +``` + +
+ + +**Patch Generation (`generatePatch`)** would perform the following: + +1. **Identify Changes**: + - Function name changed from `add` to `addNumbers` (`CONTEXT` vs. `ADD`). + - Added a new line declaring `val sum`. + - Modified the return statement to `return sum` (`DELETE` and `ADD`). + +2. **Generate Diff**: + - Categorize each line accordingly. + - Truncate any excessive unchanged lines for brevity. + +3. **Produce Patch**: + ```diff + - fun add(a: Int, b: Int): Int { + + fun addNumbers(a: Int, b: Int): Int { + + val sum = a + b + return a + b + - return a + b + + return sum + } + ``` + +**Applying Patch (`applyPatch`)** would take the original code and the above patch to produce the modified code as shown in `newCode`. + +## **10. Advantages and Considerations** + +### **Advantages**: + +- **Accuracy**: By using multiple linking strategies and considering bracket metrics, the algorithm accurately identifies and applies changes. +- **Efficiency**: Optimizations like context truncation and no-op pair removal reduce patch size and processing time. +- **Robustness**: Incorporation of Levenshtein distance allows for flexibility in handling minor discrepancies or typos. +- **Maintainability**: Clear data structures and modular functions facilitate easy maintenance and potential future enhancements. + +### **Considerations**: + +- **Complexity**: The algorithm's multiple steps and recursive linking can introduce complexity, making it essential to thoroughly test and debug. +- **Performance**: For very large files, the recursive `subsequenceLinking` and extensive logging might impact performance. Optimizations or limitations on recursion depth help mitigate this. +- **Whitespace Sensitivity**: Normalizing lines by removing all whitespace may lead to unintended matches if significant changes are solely based on formatting. + +## **Conclusion** + +The `IterativePatchUtil` implements a comprehensive and intelligent patching algorithm that meticulously compares and applies changes between two versions of text. Its multi-faceted approach, incorporating exact matches, adjacency analysis, subsequence linking, bracket metrics, and Levenshtein distance, ensures high accuracy and efficiency in generating and applying patches. While its complexity necessitates careful management and testing, the algorithm offers robust capabilities for handling nuanced changes in textual content, making it highly suitable for applications like version control systems, code editors, and collaborative document editing tools. \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt index 352ee890..822b6007 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt @@ -85,7 +85,7 @@ open class TabbedDisplay( open fun update() { log.debug("Updating container content") - if (container != null) synchronized(container) { + synchronized(container) { if (tabs.isNotEmpty() && (selectedTab < 0 || selectedTab >= tabs.size)) { selectedTab = 0 } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/DocumentParserApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/DocumentParserApp.kt index cb58f279..25f1885d 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/DocumentParserApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/DocumentParserApp.kt @@ -2,6 +2,7 @@ package com.simiacryptus.skyenet.apps.general import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ChatClient +import com.simiacryptus.jopenai.models.AnthropicModels import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.util.JsonUtil import com.simiacryptus.skyenet.TabbedDisplay @@ -30,7 +31,7 @@ open class DocumentParserApp( applicationName: String = "Document Extractor", path: String = "/pdfExtractor", val api: API = ChatClient(), - val parsingModel: ParsingModel = DefaultParsingModel(ChatModels.Claude35Sonnet, 0.1), + val parsingModel: ParsingModel = DefaultParsingModel(AnthropicModels.Claude35Sonnet, 0.1), val reader: (File) -> DocumentReader = { when { it.name.endsWith(".pdf", ignoreCase = true) -> PDFReader(it) 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 index 2c578087..eb526d79 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanAheadApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanAheadApp.kt @@ -20,7 +20,7 @@ import java.io.File open class PlanAheadApp( applicationName: String = "Task Planning v1.1", path: String = "/taskDev", - val rootFile: File, + val rootFile: File? = null, val planSettings: PlanSettings, val model: OpenAITextModel, val parsingModel: OpenAITextModel, @@ -34,7 +34,7 @@ open class PlanAheadApp( showMenubar = showMenubar, ) { override val singleInput: Boolean get() = true - override val root: File get() = rootFile + override val root: File get() = rootFile ?: super.root @Suppress("UNCHECKED_CAST") override fun initSettings(session: Session): T = planSettings.let { @@ -55,7 +55,7 @@ open class PlanAheadApp( session = session, dataStorage = dataStorage, ui = ui, - root = rootFile.toPath(), + root = dataStorage.getDataDir(user, session).toPath(), planSettings = planSettings!! ) coordinator.executeTaskBreakdownWithPrompt(JsonUtil.toJson(initialPlan), api!!) @@ -83,7 +83,7 @@ open class PlanAheadApp( session = session, dataStorage = dataStorage, ui = ui, - root = rootFile.toPath(), + root = dataStorage.getDataDir(user, session).toPath(), planSettings = planSettings!! ) val task = coordinator.ui.newTask() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanChatApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanChatApp.kt index d8a8bdef..55c633b6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanChatApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PlanChatApp.kt @@ -15,7 +15,7 @@ import java.util.* class PlanChatApp( applicationName: String = "Task Planning Chat v1.0", path: String = "/taskChat", - rootFile: File, + rootFile: File? = null, planSettings: PlanSettings, model: OpenAITextModel, parsingModel: OpenAITextModel, @@ -73,7 +73,7 @@ class PlanChatApp( try { messageHistory.add(userMessage) val planSettings = (getSettings(session, user, PlanSettings::class.java) ?: PlanSettings( - model = model, + defaultModel = model, parsingModel = parsingModel, command = planSettings.command, temperature = planSettings.temperature, @@ -88,22 +88,22 @@ class PlanChatApp( session = session, dataStorage = dataStorage, ui = ui, - root = rootFile.toPath(), + root = dataStorage.getDataDir(user, session).toPath(), planSettings = planSettings ) val mainTask = coordinator.ui.newTask() val sessionTask = ui.newTask(false).apply { mainTask.verbose(placeholder) } val api = (api as ChatClient).getChildClient().apply { - val createFile = sessionTask.createFile("api-${UUID.randomUUID()}.log") + val createFile = sessionTask.createFile(".logs/api-${UUID.randomUUID()}.log") createFile.second?.apply { logStreams += this.outputStream().buffered() - sessionTask.add("API log: $this") + sessionTask.verbose("API log: $this") } } val plan = PlanCoordinator.initialPlan( codeFiles = coordinator.codeFiles, files = coordinator.files, - root = coordinator.root, + root = dataStorage.getDataDir(user, session).toPath(), task = sessionTask, userMessage = userMessage, ui = coordinator.ui, @@ -139,14 +139,13 @@ class PlanChatApp( val respondTaskId = "respond_to_chat" tasksByID[respondTaskId] = PlanningTask.PlanTask( - description = "Respond to the user's chat message based on the executed plan", - taskType = TaskType.Inquiry, + task_description = "Respond to the user's chat message based on the executed plan", + task_type = TaskType.Inquiry, task_dependencies = tasksByID.keys.toList() ) return PlanningTask.TaskBreakdownResult( tasksByID = tasksByID, - finalTaskID = respondTaskId ) } 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 68a521b3..34a9bf23 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 @@ -8,6 +8,7 @@ import com.simiacryptus.jopenai.ChatClient import com.simiacryptus.jopenai.describe.Description import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.ImageModels +import com.simiacryptus.jopenai.models.OpenAIModels import com.simiacryptus.jopenai.proxy.ValidatedObject import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.jopenai.util.JsonUtil @@ -67,8 +68,8 @@ open class WebDevApp( data class Settings( val budget: Double? = 2.00, val tools: List = emptyList(), - val model: ChatModels = ChatModels.GPT4o, - val parsingModel: ChatModels = ChatModels.GPT35Turbo, + val model: ChatModels = OpenAIModels.GPT4o, + val parsingModel: ChatModels = OpenAIModels.GPT4oMini, ) override val settingsClass: Class<*> get() = Settings::class.java diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/parsers/DefaultParsingModel.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/parsers/DefaultParsingModel.kt index b1a7a8b0..94f82560 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/parsers/DefaultParsingModel.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/parsers/DefaultParsingModel.kt @@ -7,7 +7,7 @@ import com.simiacryptus.skyenet.core.actors.ParsedActor open class DefaultParsingModel( - private val chatModels: ChatModels, + private val parsingModel: ChatModels, private val temperature: Double ) : ParsingModel { @@ -93,7 +93,7 @@ open class DefaultParsingModel( resultClass = DocumentData::class.java, exampleInstance = exampleInstance, prompt = "", - parsingModel = chatModels, + parsingModel = parsingModel, temperature = temperature ).getParser( api, promptSuffix = promptSuffix diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractAnalysisTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractAnalysisTask.kt index 72ad1992..d98c1236 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractAnalysisTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractAnalysisTask.kt @@ -24,7 +24,7 @@ abstract class AbstractAnalysisTask( SimpleActor( name = actorName, prompt = actorPrompt, - model = planSettings.model, + model = planSettings.getTaskSettings(planTask.task_type!!).model ?: planSettings.defaultModel, temperature = planSettings.temperature, ) } @@ -65,7 +65,7 @@ abstract class AbstractAnalysisTask( autoFix = agent.planSettings.autoFix ), api = api as ChatClient, - model = agent.planSettings.model, + model = agent.planSettings.getTaskSettings(planTask.task_type!!).model ?: agent.planSettings.defaultModel, files = agent.files, command = analysisResult ).run( 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 index a0aa887f..6ce67415 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/AbstractTask.kt @@ -18,9 +18,9 @@ abstract class AbstractTask( val planTask: PlanTask ) { var state: TaskState? = TaskState.Pending - val codeFiles = mutableMapOf() + protected val codeFiles = mutableMapOf() - open val root: Path + protected open val root: Path get() = File(planSettings.workingDir).toPath() enum class TaskState { @@ -29,7 +29,7 @@ abstract class AbstractTask( Completed, } - fun getPriorCode(planProcessingState: PlanProcessingState) = + protected fun getPriorCode(planProcessingState: PlanProcessingState) = planTask.task_dependencies?.joinToString("\n\n\n") { dependency -> """ |# $dependency @@ -38,7 +38,7 @@ abstract class AbstractTask( """.trimMargin() } ?: "" - fun getInputFileCode(): String = ((planTask.input_files ?: listOf()) + (planTask.output_files ?: listOf())) + protected fun getInputFileCode(): String = ((planTask.input_files ?: listOf()) + (planTask.output_files ?: listOf())) .flatMap { pattern: String -> val matcher = FileSystems.getDefault().getPathMatcher("glob:$pattern") Files.walk(root).asSequence() @@ -69,7 +69,7 @@ abstract class AbstractTask( } } - fun acceptButtonFooter(ui: ApplicationInterface, fn: () -> Unit): String { + protected 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") { 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 index bf68b661..ee28e22e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/CommandAutoFixTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/CommandAutoFixTask.kt @@ -18,13 +18,13 @@ class CommandAutoFixTask( ) : AbstractTask(planSettings, planTask) { 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 - | ** Specify the working directory relative to the root directory - | ** Provide the command arguments in the 'commandArguments' field - | ** List input files/tasks to be examined when fixing issues - | ** Available commands: - | ${planSettings.commandAutoFixCommands?.joinToString("\n ") { "* ${File(it).name}" }} + |CommandAutoFix - Run a command and automatically fix any issues that arise + | ** Specify the command to be executed and any additional instructions + | ** Specify the working directory relative to the root directory + | ** Provide the command arguments in the 'commandArguments' field + | ** List input files/tasks to be examined when fixing issues + | ** Available commands: + |${planSettings.commandAutoFixCommands?.joinToString("\n") { " * ${File(it).name}" }} """.trimMargin() } @@ -41,70 +41,65 @@ class CommandAutoFixTask( val onComplete = { semaphore.release() } - if (!agent.planSettings.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.planTask.command?.first() - val commandAutoFixCommands = agent.planSettings.commandAutoFixCommands - val cmds = commandAutoFixCommands - ?.map { File(it) }?.associateBy { it.name } - ?.filterKeys { it.startsWith(alias ?: "") } - ?: emptyMap() - val executable = cmds.entries.firstOrNull()?.value - if (executable == null) { - throw IllegalArgumentException("Command not found: $alias") - } - val workingDirectory = (this.planTask.workingDir - ?.let { agent.root.toFile().resolve(it) } ?: agent.root.toFile()) - .apply { mkdirs() } - val outputResult = CmdPatchApp( - root = agent.root, - session = agent.session, - settings = PatchApp.Settings( - executable = executable, - arguments = this.planTask.command?.drop(1)?.joinToString(" ") ?: "", - workingDirectory = workingDirectory, - exitCodeOption = "nonzero", - additionalInstructions = "", - autoFix = agent.planSettings.autoFix - ), - api = api as ChatClient, - files = agent.files, - model = agent.planSettings.model, - ).run( - ui = agent.ui, - task = task - ) - planProcessingState.taskResult[taskId] = "Command Auto Fix completed" - task.add(if (outputResult.exitCode == 0) { - if (agent.planSettings.autoFix) { - 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 - ) { - onComplete() - } - } + Retryable(agent.ui, task = task) { + val task = agent.ui.newTask(false).apply { it.append(placeholder) } + val alias = this.planTask.execution_task?.command?.first() + val commandAutoFixCommands = agent.planSettings.commandAutoFixCommands + val cmds = commandAutoFixCommands + ?.map { File(it) }?.associateBy { it.name } + ?.filterKeys { it.startsWith(alias ?: "") } + ?: emptyMap() + val executable = cmds.entries.firstOrNull()?.value + if (executable == null) { + throw IllegalArgumentException("Command not found: $alias") + } + val workingDirectory = (this.planTask.execution_task?.workingDir + ?.let { agent.root.toFile().resolve(it) } ?: agent.root.toFile()) + .apply { mkdirs() } + val outputResult = CmdPatchApp( + root = agent.root, + session = agent.session, + settings = PatchApp.Settings( + executable = executable, + arguments = this.planTask.execution_task?.command?.drop(1)?.joinToString(" ") ?: "", + workingDirectory = workingDirectory, + exitCodeOption = "nonzero", + additionalInstructions = "", + autoFix = agent.planSettings.autoFix + ), + api = api as ChatClient, + files = agent.files, + model = agent.planSettings.getTaskSettings(planTask.task_type!!).model ?: agent.planSettings.defaultModel, + ).run( + ui = agent.ui, + task = task + ) + planProcessingState.taskResult[taskId] = "Command Auto Fix completed" + task.add(if (outputResult.exitCode == 0) { + if (agent.planSettings.autoFix) { + onComplete() + MarkdownUtil.renderMarkdown("## Auto-applied Command Auto Fix\n", ui = agent.ui) } else { MarkdownUtil.renderMarkdown( - "## Command Auto Fix Failed\n", + "## Command Auto Fix Result\n", ui = agent.ui ) + acceptButtonFooter( agent.ui ) { onComplete() } - }) - task.placeholder - } + } + } else { + MarkdownUtil.renderMarkdown( + "## Command Auto Fix Failed\n", + ui = agent.ui + ) + acceptButtonFooter( + agent.ui + ) { + onComplete() + } + }) + task.placeholder } try { semaphore.acquire() 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 index 4a8df9a1..6d659edc 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/DocumentationTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/DocumentationTask.kt @@ -16,8 +16,8 @@ class DocumentationTask( ) : AbstractTask(planSettings, planTask) { override fun promptSegment(): String { return """ - |Documentation - Generate documentation - | ** List input files/tasks to be examined + Documentation - Generate documentation + ** List input files/tasks to be examined """.trimMargin() } @@ -25,12 +25,12 @@ class DocumentationTask( 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. + 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 = planSettings.model, + model = planSettings.getTaskSettings(planTask.task_type!!).model ?: planSettings.defaultModel, temperature = planSettings.temperature, ) } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/FileModificationTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/FileModificationTask.kt index 7a46e218..2256fd89 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/FileModificationTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/FileModificationTask.kt @@ -19,59 +19,59 @@ class FileModificationTask( SimpleActor( name = "FileModification", prompt = """ - |Generate patches for existing files or create new files based on the given requirements and context. - |For existing files: - |- Ensure modifications are efficient, maintain readability, and adhere to coding standards. - |- Carefully review the existing code and project structure to ensure changes are consistent and do not introduce bugs. - |- Consider the impact of modifications on other parts of the codebase. + Generate patches for existing files or create new files based on the given requirements and context. + For existing files: + Ensure modifications are efficient, maintain readability, and adhere to coding standards. + Carefully review the existing code and project structure to ensure changes are consistent and do not introduce bugs. + Consider the impact of modifications on other parts of the codebase. - |For new files: - |- 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. + For new files: + 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. - |Provide a summary of the changes made. + Provide a summary of the changes made. - |Response format: - |- For existing files: Use ${TRIPLE_TILDE}diff code blocks with a header specifying the file path. - |- For new files: Use ${TRIPLE_TILDE} code blocks with a header specifying the new file path. - |- The diff format should use + for line additions, - for line deletions. - |- Include 2 lines of context before and after every change in diffs. - |- Separate code blocks with a single blank line. - |- For new files, specify the language for syntax highlighting after the opening triple backticks. + Response format: + For existing files: Use ${TRIPLE_TILDE}diff code blocks with a header specifying the file path. + For new files: Use ${TRIPLE_TILDE} code blocks with a header specifying the new file path. + The diff format should use + for line additions, - for line deletions. + Include 2 lines of context before and after every change in diffs. + Separate code blocks with a single blank line. + For new files, specify the language for syntax highlighting after the opening triple backticks. - |Example: + Example: - |Here are the modifications: + Here are the modifications: - |### src/utils/existingFile.js - |${TRIPLE_TILDE}diff - | // Existing utility functions - | function existingFunction() { - |- return 'old result'; - |+ return 'new result'; + ### src/utils/existingFile.js + ${TRIPLE_TILDE}diff + // Existing utility functions + function existingFunction() { + return 'old result'; + return 'new result'; | } - |${TRIPLE_TILDE} + ${TRIPLE_TILDE} - |### src/utils/newFile.js - |${TRIPLE_TILDE}js - |// New utility functions - |function newFunction() { - | return 'new functionality'; + ### src/utils/newFile.js + ${TRIPLE_TILDE}js + // New utility functions + function newFunction() { + return 'new functionality'; |} - |${TRIPLE_TILDE} + ${TRIPLE_TILDE} """.trimMargin(), - model = planSettings.model, + model = planSettings.getTaskSettings(planTask.task_type!!).model ?: planSettings.defaultModel, temperature = planSettings.temperature, ) } override fun promptSegment(): String { return """ - |FileModification - Modify existing files or create new files - | ** For each file, specify the relative file path and the goal of the modification or creation - | ** List input files/tasks to be examined when designing the modifications or new files + FileModification - Modify existing files or create new files + ** For each file, specify the relative file path and the goal of the modification or creation + ** List input files/tasks to be examined when designing the modifications or new files """.trimMargin() } @@ -97,7 +97,7 @@ class FileModificationTask( JsonUtil.toJson(plan), getPriorCode(planProcessingState), getInputFileCode(), - this.planTask.description ?: "", + this.planTask.task_description ?: "", ).filter { it.isNotBlank() }, api ) planProcessingState.taskResult[taskId] = codeResult @@ -112,7 +112,7 @@ class FileModificationTask( }, ui = agent.ui, api = api, - shouldAutoApply = { true } + shouldAutoApply = { agent.planSettings.autoFix } ) task.complete() onComplete() diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/ForeachTask.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/ForeachTask.kt index 6ea85da7..8292cf18 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/ForeachTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/ForeachTask.kt @@ -29,14 +29,14 @@ ForeachTask - Execute a task for each item in a list task: SessionTask, api: API ) { - val items = planTask.foreachItems ?: throw RuntimeException("No items specified for ForeachTask") - val subTasks = planTask.subTasksByID ?: throw RuntimeException("No subTasks specified for ForeachTask") + val items = planTask.foreach_task?.foreach_items ?: throw RuntimeException("No items specified for ForeachTask") + val subTasks = planTask.foreach_task?.foreach_subplan ?: throw RuntimeException("No subTasks specified for ForeachTask") val subPlanTask = agent.ui.newTask(false) task.add(subPlanTask.placeholder) items.forEachIndexed { index, item -> val itemSubTasks = subTasks.mapValues { (_, subTaskPlan) -> - subTaskPlan.copy(description = "${subTaskPlan.description} - Item $index: $item") + subTaskPlan.copy(task_description = "${subTaskPlan.task_description} - Item $index: $item") } val itemPlanProcessingState = PlanProcessingState(itemSubTasks.toMutableMap()) agent.executePlan( @@ -50,7 +50,6 @@ ForeachTask - Execute a task for each item in a list userMessage = "$userMessage\nProcessing item $index: $item", plan = object : TaskBreakdownInterface { override val tasksByID = itemSubTasks - override val finalTaskID = null }, api = api ) 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 index 980aa67e..fdc634c6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/InquiryTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/InquiryTask.kt @@ -21,30 +21,28 @@ class InquiryTask( 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. + 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 (!planSettings.getTaskSettings(TaskType.TaskPlanning).enabled) "" else "TaskPlanning, " - }${ - if (!planSettings.getTaskSettings(TaskType.RunShellCommand).enabled) "" 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. + 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 (${ + planSettings.taskSettings.filter { it.value.enabled }.keys.joinToString(", ") + }). + This will ensure that the inquiries are tailored to assist in the planning and execution of tasks within the system's framework. """.trimMargin(), - model = planSettings.model, + model = planSettings.getTaskSettings(planTask.task_type!!).model ?: planSettings.defaultModel, temperature = planSettings.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 + 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() } @@ -69,7 +67,7 @@ class InquiryTask( val inquiryResult = if (planSettings.allowBlocking) Discussable( task = task, - userMessage = { "Expand ${this.planTask.description ?: ""}\n${JsonUtil.toJson(data = this)}" }, + userMessage = { "Expand ${this.planTask.task_description ?: ""}\n${JsonUtil.toJson(data = this)}" }, heading = "", initialResponse = { it: String -> inquiryActor.answer(toInput(it), api = api) }, outputFn = { design: String -> @@ -77,7 +75,7 @@ class InquiryTask( }, ui = agent.ui, reviseResponse = { userMessages: List> -> - val inStr = "Expand ${this.planTask.description ?: ""}\n${JsonUtil.toJson(data = this)}" + val inStr = "Expand ${this.planTask.task_description ?: ""}\n${JsonUtil.toJson(data = this)}" val messages = userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } .toTypedArray() inquiryActor.respond( @@ -89,7 +87,7 @@ class InquiryTask( atomicRef = AtomicReference(), semaphore = Semaphore(0), ).call() else inquiryActor.answer( - toInput("Expand ${this.planTask.description ?: ""}\n${JsonUtil.toJson(data = this)}"), + toInput("Expand ${this.planTask.task_description ?: ""}\n${JsonUtil.toJson(data = this)}"), api = api ).apply { task.add(MarkdownUtil.renderMarkdown(this, ui = agent.ui)) 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 index 5592157b..56ea7090 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanCoordinator.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanCoordinator.kt @@ -5,7 +5,6 @@ import com.simiacryptus.diff.FileValidationUtils import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ChatClient -import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.jopenai.util.JsonUtil import com.simiacryptus.skyenet.Discussable import com.simiacryptus.skyenet.TabbedDisplay @@ -88,6 +87,13 @@ class PlanCoordinator( userMessage: String, api: API ): PlanProcessingState { + val api = (api as ChatClient).getChildClient().apply { + val createFile = task.createFile(".logs/api-${UUID.randomUUID()}.log") + createFile.second?.apply { + logStreams += this.outputStream().buffered() + task.verbose("API log: $this") + } + } val planProcessingState = newState(plan) try { val diagramTask = ui.newTask(false).apply { task.add(placeholder) } @@ -133,10 +139,10 @@ class PlanCoordinator( ) { val sessionTask = ui.newTask(false).apply { task.add(placeholder) } val api = (api as ChatClient).getChildClient().apply { - val createFile = sessionTask.createFile("api-${UUID.randomUUID()}.log") + val createFile = sessionTask.createFile(".logs/api-${UUID.randomUUID()}.log") createFile.second?.apply { logStreams += this.outputStream().buffered() - sessionTask.add("API log: $this") + sessionTask.verbose("API log: $this") } } val taskTabs = object : TabbedDisplay(sessionTask) { @@ -177,7 +183,7 @@ class PlanCoordinator( val newTask = ui.newTask(false) planProcessingState.uitaskMap[taskId] = newTask val subtask = planProcessingState.subTasks[taskId] - val description = subtask?.description + val description = subtask?.task_description log.debug("Creating task tab: $taskId ${System.identityHashCode(subtask)} $description") taskTabs[description ?: taskId] = newTask.placeholder } @@ -187,7 +193,7 @@ class PlanCoordinator( val subTask = planProcessingState.subTasks[taskId] ?: throw RuntimeException("Task not found: $taskId") planProcessingState.taskFutures[taskId] = pool.submit { subTask.state = AbstractTask.TaskState.Pending - taskTabs.update() + //taskTabs.update() log.debug("Awaiting dependencies: ${subTask.task_dependencies?.joinToString(", ") ?: ""}") subTask.task_dependencies ?.associate { it to planProcessingState.taskFutures[it] } @@ -199,8 +205,8 @@ class PlanCoordinator( } } subTask.state = AbstractTask.TaskState.InProgress - taskTabs.update() - log.debug("Running task: ${System.identityHashCode(subTask)} ${subTask.description}") + //taskTabs.update() + log.debug("Running task: ${System.identityHashCode(subTask)} ${subTask.task_description}") val task1 = planProcessingState.uitaskMap.get(taskId) ?: ui.newTask(false).apply { taskTabs[taskId] = placeholder } @@ -215,19 +221,26 @@ class PlanCoordinator( task1.add( MarkdownUtil.renderMarkdown( """ - ## Task `${taskId}` - ${subTask.description ?: ""} - | - |${TRIPLE_TILDE}json - |${JsonUtil.toJson(data = subTask)/*.indent(" ")*/} - |$TRIPLE_TILDE - | - |### Dependencies: - |${dependencies.joinToString("\n") { "- $it" }} - | + |## Task `${taskId}` + |${subTask.task_description ?: ""} + | + |${TRIPLE_TILDE}json + |${JsonUtil.toJson(data = subTask)/*.indent(" ")*/} + |$TRIPLE_TILDE + | + |### Dependencies: + |${dependencies.joinToString("\n") { "- $it" }} + | """.trimMargin(), ui = ui ) ) + val api = (api as ChatClient).getChildClient().apply { + val createFile = task1.createFile(".logs/api-${UUID.randomUUID()}.log") + createFile.second?.apply { + logStreams += this.outputStream().buffered() + task1.verbose("API log: $this") + } + } getImpl(planSettings, subTask).run( agent = this, taskId = taskId, @@ -274,6 +287,13 @@ class PlanCoordinator( planSettings: PlanSettings, api: API ): PlanUtil.TaskBreakdownWithPrompt { + val api = (api as ChatClient).getChildClient().apply { + val createFile = task.createFile(".logs/api-${UUID.randomUUID()}.log") + createFile.second?.apply { + logStreams += this.outputStream().buffered() + task.verbose("API log: $this") + } + } val toInput = inputFn(codeFiles, files, root) return if (planSettings.allowBlocking) Discussable( @@ -302,9 +322,7 @@ class PlanCoordinator( newPlan( api, planSettings, - toInput(userMessage), - userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()) + userMessages.map { it.first }) }, ).call().let { PlanUtil.TaskBreakdownWithPrompt( @@ -329,18 +347,15 @@ class PlanCoordinator( private fun newPlan( api: API, planSettings: PlanSettings, - inStrings: List, - messages: Array = inStrings.map { - ApiModel.ChatMessage( - ApiModel.Role.user, - it.toContentList() - ) - }.toTypedArray() - ) = planSettings.planningActor().respond( - messages = messages, - input = inStrings, - api = api - ) as ParsedResponse + inStrings: List + ): ParsedResponse { + val planningActor = planSettings.planningActor() + return planningActor.respond( + messages = planningActor.chatMessages(inStrings), + input = inStrings, + api = api + ) as ParsedResponse + } private fun inputFn( @@ -350,16 +365,16 @@ class PlanCoordinator( ) = { str: String -> listOf( if (!codeFiles.all { it.key.toFile().isFile } || codeFiles.size > 2) """ - | Files: - | ${codeFiles.keys.joinToString("\n") { "* $it" }} + |Files: + |${codeFiles.keys.joinToString("\n") { "* $it" }} """.trimMargin() else { files.joinToString("\n\n") { val path = root.relativize(it.toPath()) """ - |## $path - | - |${(codeFiles[path] ?: "").let { "$TRIPLE_TILDE\n${it/*.indent(" ")*/}\n$TRIPLE_TILDE" }} - """.trimMargin() + |## $path + | + |${(codeFiles[path] ?: "").let { "$TRIPLE_TILDE\n${it/*.indent(" ")*/}\n$TRIPLE_TILDE" }} + """.trimMargin() } }, str diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanProcessingState.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanProcessingState.kt index b8e0b10a..800c4735 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanProcessingState.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanProcessingState.kt @@ -6,7 +6,7 @@ import java.util.concurrent.Future data class PlanProcessingState( val subTasks: Map, val tasksByDescription: MutableMap = subTasks.entries.toTypedArray() - .associate { it.value.description to it.value }.toMutableMap(), + .associate { it.value.task_description to it.value }.toMutableMap(), val taskIdProcessingQueue: MutableList = PlanUtil.executionOrder(subTasks).toMutableList(), val taskResult: MutableMap = mutableMapOf(), val completedTasks: MutableList = mutableListOf(), diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanSettings.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanSettings.kt index 29683380..79d2dbc6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanSettings.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanSettings.kt @@ -8,35 +8,32 @@ import com.simiacryptus.skyenet.apps.plan.TaskType.Companion.getAvailableTaskTyp import com.simiacryptus.skyenet.core.actors.ParsedActor data class TaskSettings( - var enabled: Boolean = false + var enabled: Boolean = false, + var model: OpenAITextModel? = null ) open class PlanSettings( - var model: OpenAITextModel, - val parsingModel: OpenAITextModel, + var defaultModel: OpenAITextModel, + var parsingModel: OpenAITextModel, val command: List = listOf(if (isWindows) "powershell" else "bash"), var temperature: Double = 0.2, val budget: Double = 2.0, - val taskSettings: MutableMap = mutableMapOf(), + val taskSettings: MutableMap = TaskType.values().associateWith { taskType -> + TaskSettings( + when (taskType) { + TaskType.FileModification, TaskType.Inquiry -> true + else -> false + } + ) + }.toMutableMap(), var autoFix: Boolean = false, var allowBlocking: Boolean = true, - var enableCommandAutoFix: Boolean = false, var commandAutoFixCommands: List? = listOf(), val env: Map? = mapOf(), val workingDir: String? = ".", val language: String? = if (isWindows) "powershell" else "bash", ) { - init { - TaskType.values().forEach { taskType -> - taskSettings[taskType] = TaskSettings( - when (taskType) { - TaskType.FileModification, TaskType.Inquiry -> true - else -> false - } - ) - } - } fun getTaskSettings(taskType: TaskType): TaskSettings = taskSettings[taskType] ?: TaskSettings() @@ -46,7 +43,7 @@ open class PlanSettings( } fun copy( - model: OpenAITextModel = this.model, + model: OpenAITextModel = this.defaultModel, parsingModel: OpenAITextModel = this.parsingModel, command: List = this.command, temperature: Double = this.temperature, @@ -54,13 +51,12 @@ open class PlanSettings( taskSettings: MutableMap = this.taskSettings, autoFix: Boolean = this.autoFix, allowBlocking: Boolean = this.allowBlocking, - enableCommandAutoFix: Boolean = this.enableCommandAutoFix, commandAutoFixCommands: List? = this.commandAutoFixCommands, env: Map? = this.env, workingDir: String? = this.workingDir, language: String? = this.language, ) = PlanSettings( - model = model, + defaultModel = model, parsingModel = parsingModel, command = command, temperature = temperature, @@ -68,38 +64,78 @@ open class PlanSettings( taskSettings = taskSettings, autoFix = autoFix, allowBlocking = allowBlocking, - enableCommandAutoFix = enableCommandAutoFix, commandAutoFixCommands = commandAutoFixCommands, env = env, workingDir = workingDir, language = language, ) - open 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(this).joinToString("\n") { "* ${it.promptSegment()}" }} - | - |${if (this.getTaskSettings(TaskType.TaskPlanning).enabled) "Do not start your plan with a plan to plan!\n" else ""} - """.trimMargin(), - model = this.model, - parsingModel = this.parsingModel, - temperature = this.temperature, - describer = describer(), + var exampleInstance = TaskBreakdownResult( + tasksByID = mapOf( + "1" to PlanningTask.PlanTask( + task_description = "Task 1", + task_type = TaskType.CommandAutoFix, + task_dependencies = listOf(), + execution_task = PlanningTask.ExecutionTask( + command = listOf("npx", "create-react-app", ".", "--template", "typescript"), + workingDir = ".", + ) + ), + "2" to PlanningTask.PlanTask( + task_description = "Task 2", + task_type = TaskType.FileModification, + task_dependencies = listOf("1"), + input_files = listOf("input2.txt"), + output_files = listOf("output2.txt"), + ), + "3" to PlanningTask.PlanTask( + task_description = "Task 3", + task_type = TaskType.TaskPlanning, + task_dependencies = listOf("2"), + input_files = listOf("input3.txt"), + ) + ), ) + open fun planningActor(): ParsedActor { + val planTaskSettings = this.getTaskSettings(TaskType.TaskPlanning) + return ParsedActor( + name = "TaskBreakdown", + resultClass = TaskBreakdownResult::class.java, + exampleInstance = exampleInstance, + prompt = """ + |Given a user request, identify and list smaller, actionable tasks that can be directly implemented in code. + | + |For each task: + |* Detail files input and output file + |* Describe task execution dependencies and order + |* Provide a brief description of the task + |* Specify important interface and integration details (each task will run independently off of a copy of this plan) + | + |Tasks can be of the following types: + |${getAvailableTaskTypes(this).joinToString("\n") { "* ${it.promptSegment()}" }} + | + |Creating directories and initializing source control are out of scope. + |${if (planTaskSettings.enabled) "Do not start your plan with a plan to plan!\n" else ""} + """.trimMargin(), + model = planTaskSettings.model ?: this.defaultModel, + parsingModel = this.parsingModel, + temperature = this.temperature, + describer = describer(), + ) + } + open fun describer() = object : AbbrevWhitelistYamlDescriber( "com.simiacryptus", "com.github.simiacryptus" ) { override val includeMethods: Boolean get() = false - } - -} + override fun getEnumValues(clazz: Class<*>): List { + return if (clazz == TaskType::class.java) { + taskSettings.filter { it.value.enabled }.map { it.key.toString() } + } else { + super.getEnumValues(clazz) + } + } + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanUtil.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanUtil.kt index 687c554a..f88861c5 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanUtil.kt @@ -2,11 +2,13 @@ package com.simiacryptus.skyenet.apps.plan import com.simiacryptus.jopenai.util.JsonUtil import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.apps.plan.AbstractTask.TaskState import com.simiacryptus.skyenet.apps.plan.PlanningTask.PlanTask import com.simiacryptus.skyenet.apps.plan.PlanningTask.TaskBreakdownInterface import com.simiacryptus.skyenet.webui.application.ApplicationInterface import com.simiacryptus.skyenet.webui.util.MarkdownUtil import java.util.* +import java.util.concurrent.ConcurrentHashMap object PlanUtil { @@ -14,7 +16,7 @@ object PlanUtil { ui: ApplicationInterface, taskMap: Map ) = MarkdownUtil.renderMarkdown( - """ + """ |## Sub-Plan Task Dependency Graph |${TRIPLE_TILDE}mermaid |${buildMermaidGraph(taskMap)} @@ -80,46 +82,71 @@ object PlanUtil { .replace("\"", "\\\"") .let { '"' + it + '"' } + // Cache for memoizing buildMermaidGraph results + private val mermaidGraphCache = ConcurrentHashMap() + private val mermaidExceptionCache = ConcurrentHashMap() + 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") + // Generate a unique key based on the subTasks map + val cacheKey = JsonUtil.toJson(subTasks) + // Return cached result if available + mermaidGraphCache[cacheKey]?.let { return it } + mermaidExceptionCache[cacheKey]?.let { throw it } + try { + val graphBuilder = StringBuilder("graph TD;\n") + subTasks.forEach { (taskId, task) -> + val sanitizedTaskId = sanitizeForMermaid(taskId) + val taskType = task.task_type?.name ?: "Unknown" + val escapedDescription = escapeMermaidCharacters(task.task_description ?: "") + val style = when (task.state) { + TaskState.Completed -> ":::completed" + 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") + val graph = graphBuilder.toString() + mermaidGraphCache[cacheKey] = graph + return graph + } catch (e: Exception) { + mermaidExceptionCache[cacheKey] = e + throw e } - 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() } fun filterPlan(retries: Int = 3, fn: () -> TaskBreakdownInterface): TaskBreakdownInterface { val obj = fn() var tasksByID = obj.tasksByID?.filter { (k, v) -> when { - v.taskType == TaskType.TaskPlanning && v.task_dependencies.isNullOrEmpty() -> false + v.task_type == TaskType.TaskPlanning && v.task_dependencies.isNullOrEmpty() -> + if (retries <= 0) { + log.warn("TaskPlanning task $k has no dependencies: " + JsonUtil.toJson(obj)) + true + } else { + log.info("TaskPlanning task $k has no dependencies") + return filterPlan(retries - 1, fn) + } else -> true } - }?.map { + } ?: emptyMap() + tasksByID = tasksByID.map { it.key to it.value.copy( - task_dependencies = it.value.task_dependencies?.filter { it in (obj.tasksByID?.keys ?: setOf()) } + task_dependencies = it.value.task_dependencies?.filter { it in tasksByID.keys }, + state = TaskState.Pending ) - }?.toMap() ?: emptyMap() + }.toMap() try { executionOrder(tasksByID) } catch (e: RuntimeException) { @@ -134,12 +161,7 @@ object PlanUtil { return if (tasksByID.size == obj.tasksByID?.size) { obj } else filterPlan { - tasksByID = tasksByID.mapValues { (_, v) -> - v.copy( - task_dependencies = v.task_dependencies?.filter { it in tasksByID.keys } - ) - } - PlanningTask.TaskBreakdownResult(tasksByID, obj.finalTaskID) + PlanningTask.TaskBreakdownResult(tasksByID) } } 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 index 79120412..62ce097c 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanningTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/PlanningTask.kt @@ -19,42 +19,61 @@ class PlanningTask( planTask: PlanTask ) : AbstractTask(planSettings, planTask) { - interface TaskBreakdownInterface { val tasksByID: Map? - val finalTaskID: String? } data class TaskBreakdownResult( + @Description("A map where each task ID is associated with its corresponding PlanTask object. Crucial for defining task relationships and information flow.") override val tasksByID: Map? = null, - override val finalTaskID: String? = null, ) : TaskBreakdownInterface data class PlanTask( - val description: String? = null, - val taskType: TaskType? = null, + @Description("A detailed description of the specific task to be performed, including its role in the overall plan and its dependencies on other tasks.") + val task_description: String? = null, + @Description("An enumeration indicating the type of task to be executed.") + val task_type: TaskType? = null, + @Description("A list of IDs of tasks that must be completed before this task can be executed. This defines upstream dependencies ensuring proper task order and information flow.") var task_dependencies: List? = null, + @Description("A list of file paths specifying the input files required by this task. These may be outputs from dependent tasks, facilitating data transfer between tasks.") val input_files: List? = null, + @Description("A list of file paths specifying the output files generated by this task. These may serve as inputs for subsequent tasks, enabling information sharing.") val output_files: List? = null, - var state: TaskState? = null, - @Description("Command and arguments (in list form) for the task") + @Description("The current execution state of the task. Important for coordinating task execution and managing dependencies.") + var state: TaskState? = TaskState.Pending, + @Description("Only applicable in Foreach tasks - details specific to the Foreach task.") + val foreach_task: ForEachTask? = null, + @Description("Only applicable in CommandAutoFix tasks - details specific to the CommandAutoFix task.") + val execution_task: ExecutionTask? = null, + ) + + data class ForEachTask( + @Description("A list of items over which the ForEach task will iterate. (Only applicable for ForeachTask tasks) Can be used to process outputs from previous tasks.") + val foreach_items: List? = null, + @Description("A map of sub-task IDs to PlanTask objects to be executed for each item. (Only applicable for ForeachTask tasks) Allows for complex task dependencies and information flow within iterations.") + val foreach_subplan: Map? = null, + ) + + data class ExecutionTask( + @Description("The command line for the task (Only applicable in CommandAutoFix tasks).") val command: List? = null, - @Description("Working directory for the command execution") + @Description("The working directory relative to the root directory (e.g., \".\" or \"./subdir\") (Only applicable for CommandAutoFix and RunShellCommand tasks)") val workingDir: String? = null, - @Description("List of items to iterate over") - val foreachItems: List? = null, - @Description("When applicable, sub-tasks to execute") - val subTasksByID: Map? = null, ) private val taskBreakdownActor by lazy { planSettings.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 + |Task Planning: + | * Perform high-level planning and organization of tasks. + | * Decompose the overall goal into smaller, actionable tasks based on current information, ensuring proper information flow between tasks. + | * Specify prior tasks and the overall goal of the task, emphasizing dependencies to ensure each task is connected with its upstream and downstream tasks. + | * Dynamically break down tasks as new information becomes available. + | * Carefully consider task dependencies to ensure efficient information transfer and coordination between tasks. + | * Design the task structure to maximize parallel execution where possible, while respecting necessary dependencies. + | * **Note**: A planning task should refine the plan based on new information, optimizing task relationships and dependencies, and should not initiate execution. + | * Ensure that each task utilizes the outputs or side effects of its upstream tasks, and provides outputs or side effects for its downstream tasks. """.trimMargin() } @@ -67,7 +86,6 @@ class PlanningTask( task: SessionTask, api: API ) { - if (!agent.planSettings.getTaskSettings(TaskType.TaskPlanning).enabled) 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, @@ -80,7 +98,7 @@ class PlanningTask( createSubPlanDiscussable(agent, task, userMessage, ::toInput, api).call().obj } else { val design = taskBreakdownActor.answer( - toInput("Expand ${planTask.description ?: ""}"), + toInput("Expand ${planTask.task_description ?: ""}"), api = api ) render( @@ -105,7 +123,7 @@ class PlanningTask( ): Discussable> { return Discussable( task = task, - userMessage = { "Expand ${planTask.description ?: ""}" }, + userMessage = { "Expand ${planTask.task_description ?: ""}" }, heading = "", initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, outputFn = { design: ParsedResponse -> @@ -123,7 +141,7 @@ class PlanningTask( taskBreakdownActor.respond( messages = userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } .toTypedArray(), - input = toInput("Expand ${planTask.description ?: ""}\n${JsonUtil.toJson(this)}"), + input = toInput("Expand ${planTask.task_description ?: ""}\n${JsonUtil.toJson(this)}"), api = api ) }, 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 index becef382..ddb33191 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/RunShellCommandTask.kt @@ -22,27 +22,27 @@ class RunShellCommandTask( name = "RunShellCommand", interpreterClass = ProcessInterpreter::class, details = """ - Execute the following shell command(s) and provide the output. Ensure to handle any errors or exceptions gracefully. + |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. + |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 (planSettings.env ?: emptyMap()), - "workingDir" to (planTask.workingDir?.let { File(it).absolutePath } ?: File(planSettings.workingDir).absolutePath), + "workingDir" to (planTask.execution_task?.workingDir?.let { File(it).absolutePath } ?: File(planSettings.workingDir).absolutePath), "language" to (planSettings.language ?: "bash"), "command" to planSettings.command, ), - model = planSettings.model, + model = planSettings.getTaskSettings(planTask.task_type!!).model ?: planSettings.defaultModel, temperature = planSettings.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 - ** Optionally specify a working directory for the command execution + |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 + | ** Optionally specify a working directory for the command execution """.trimMargin() } @@ -55,7 +55,6 @@ class RunShellCommandTask( task: SessionTask, api: API ) { - if (!agent.planSettings.getTaskSettings(TaskType.RunShellCommand).enabled) throw RuntimeException("Shell command task is disabled") val semaphore = Semaphore(0) object : CodingAgent( api = api, @@ -79,11 +78,11 @@ class RunShellCommandTask( 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!! }} +
+ ${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()) @@ -103,9 +102,10 @@ class RunShellCommandTask( |${response.code} |$TRIPLE_TILDE | - |$TRIPLE_TILDE + |${TRIPLE_TILDE} |${response.renderedResponse} - |$TRIPLE_TILDE + |${TRIPLE_TILDE} + | """.trimMargin() } semaphore.release() 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 index db82c4d1..b027207f 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/TaskType.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/TaskType.kt @@ -1,10 +1,17 @@ package com.simiacryptus.skyenet.apps.plan +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.simiacryptus.util.DynamicEnum +import com.simiacryptus.util.DynamicEnumDeserializer +import com.simiacryptus.util.DynamicEnumSerializer -class TaskType(val name: String) { +@JsonDeserialize(using = TaskTypeDeserializer::class) +@JsonSerialize(using = TaskTypeSerializer::class) +class TaskType(name: String) : DynamicEnum(name) { companion object { - private val registry = mutableMapOf() private val taskConstructors = mutableMapOf AbstractTask>() + val TaskPlanning = TaskType("TaskPlanning") val Inquiry = TaskType("Inquiry") val FileModification = TaskType("FileModification") @@ -20,34 +27,32 @@ class TaskType(val name: String) { val ForeachTask = TaskType("ForeachTask") init { - // Register built-in task types - register(TaskPlanning) { settings, task -> PlanningTask(settings, task) } - register(Inquiry) { settings, task -> InquiryTask(settings, task) } - register(FileModification) { settings, task -> FileModificationTask(settings, task) } - register(Documentation) { settings, task -> DocumentationTask(settings, task) } - register(RunShellCommand) { settings, task -> RunShellCommandTask(settings, task) } - register(CommandAutoFix) { settings, task -> CommandAutoFixTask(settings, task) } - register(CodeReview) { settings, task -> CodeReviewTask(settings, task) } - register(TestGeneration) { settings, task -> TestGenerationTask(settings, task) } - register(Optimization) { settings, task -> CodeOptimizationTask(settings, task) } - register(SecurityAudit) { settings, task -> SecurityAuditTask(settings, task) } - register(PerformanceAnalysis) { settings, task -> PerformanceAnalysisTask(settings, task) } - register(RefactorTask) { settings, task -> RefactorTask(settings, task) } - register(ForeachTask) { settings, task -> ForeachTask(settings, task) } + registerConstructor(CommandAutoFix) { settings, task -> CommandAutoFixTask(settings, task) } + registerConstructor(Inquiry) { settings, task -> InquiryTask(settings, task) } + registerConstructor(FileModification) { settings, task -> FileModificationTask(settings, task) } + registerConstructor(Documentation) { settings, task -> DocumentationTask(settings, task) } + registerConstructor(RunShellCommand) { settings, task -> RunShellCommandTask(settings, task) } + registerConstructor(CodeReview) { settings, task -> CodeReviewTask(settings, task) } + registerConstructor(TestGeneration) { settings, task -> TestGenerationTask(settings, task) } + registerConstructor(Optimization) { settings, task -> CodeOptimizationTask(settings, task) } + registerConstructor(SecurityAudit) { settings, task -> SecurityAuditTask(settings, task) } + registerConstructor(PerformanceAnalysis) { settings, task -> PerformanceAnalysisTask(settings, task) } + registerConstructor(RefactorTask) { settings, task -> RefactorTask(settings, task) } + registerConstructor(ForeachTask) { settings, task -> ForeachTask(settings, task) } + registerConstructor(TaskPlanning) { settings, task -> PlanningTask(settings, task) } } - fun register(taskType: TaskType, constructor: (PlanSettings, PlanningTask.PlanTask) -> AbstractTask) { - registry[taskType.name] = taskType + private fun registerConstructor( + taskType: TaskType, + constructor: (PlanSettings, PlanningTask.PlanTask) -> AbstractTask + ) { taskConstructors[taskType] = constructor + register(taskType) } - fun getTaskType(name: String): TaskType? { - return registry[name] - } - - fun values(): Collection = registry.values + fun values() = values(TaskType::class.java) fun getImpl(planSettings: PlanSettings, planTask: PlanningTask.PlanTask): AbstractTask { - val taskType = planTask.taskType ?: throw RuntimeException("Task type is null") + val taskType = planTask.task_type ?: throw RuntimeException("Task type is null") if (!planSettings.getTaskSettings(taskType).enabled) { throw DisabledTaskException(taskType) } @@ -56,12 +61,14 @@ class TaskType(val name: String) { return constructor(planSettings, planTask) } - fun getAvailableTaskTypes(planSettings: PlanSettings): List = TaskType.values().filter { + fun getAvailableTaskTypes(planSettings: PlanSettings) = values().filter { planSettings.getTaskSettings(it).enabled - }.map { getImpl(planSettings, PlanningTask.PlanTask(taskType = it)) } + }.map { getImpl(planSettings, PlanningTask.PlanTask(task_type = it)) } - fun valueOf(name: String): TaskType { - return registry[name] ?: throw IllegalArgumentException("No enum constant $name") - } + fun valueOf(name: String): TaskType = valueOf(TaskType::class.java, name) + private fun register(taskType: TaskType) = register(TaskType::class.java, taskType) } -} \ No newline at end of file +} + +class TaskTypeSerializer : DynamicEnumSerializer(TaskType::class.java) +class TaskTypeDeserializer : DynamicEnumDeserializer(TaskType::class.java) \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/planning_agent.md b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/planning_agent.md new file mode 100644 index 00000000..115bef2f --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/plan/planning_agent.md @@ -0,0 +1,759 @@ +# **Plan Feature User and Developer Guide** + +Welcome to the comprehensive guide for the **Plan** feature, designed to assist both end-users and developers in +effectively utilizing and extending the functionality provided by the `com.simiacryptus.skyenet.apps.plan` package. This +guide covers everything from high-level overviews to detailed implementation insights. + + +* [**Plan Feature User and Developer Guide**](#plan-feature-user-and-developer-guide) + * [**Introduction**](#introduction) + * [**Features Overview**](#features-overview) + * [**User Guide**](#user-guide) + * [**Accessing the Plan Feature**](#accessing-the-plan-feature) + * [**Creating and Managing Plans**](#creating-and-managing-plans) + * [**Understanding Task Dependencies**](#understanding-task-dependencies) + * [**Viewing Execution Logs**](#viewing-execution-logs) + * [**Developer Guide**](#developer-guide) + * [**Architecture Overview**](#architecture-overview) + * [**Task Types**](#task-types) + * [**1. CommandAutoFixTask**](#1-commandautofixtask) + * [**2. InquiryTask**](#2-inquirytask) + * [**3. FileModificationTask**](#3-filemodificationtask) + * [**4. RunShellCommandTask**](#4-runshellcommandtask) + * [**5. RefactorTask**](#5-refactortask) + * [**6. SecurityAuditTask**](#6-securityaudittask) + * [**7. CodeOptimizationTask**](#7-codeoptimizationtask) + * [**8. CodeReviewTask**](#8-codereviewtask) + * [**9. DocumentationTask**](#9-documentationtask) + * [**10. TestGenerationTask**](#10-testgenerationtask) + * [**Adding New Task Types**](#adding-new-task-types) + * [**Configuring Plan Settings**](#configuring-plan-settings) + * [**Extending PlanCoordinator**](#extending-plancoordinator) + * [**Utilizing PlanUtil**](#utilizing-planutil) + * [**Handling Asynchronous Task Execution**](#handling-asynchronous-task-execution) + * [**Customizing Task Execution**](#customizing-task-execution) + * [**Best Practices**](#best-practices) + * [**Troubleshooting**](#troubleshooting) + * [**Frequently Asked Questions (FAQs)**](#frequently-asked-questions-faqs) + + +## **Introduction** + +The **Plan** feature is a sophisticated system designed to break down high-level user requests into manageable, +actionable tasks. It leverages OpenAI's capabilities to automate task planning, execution, and management, ensuring +efficient workflow and seamless integration with existing systems. + +--- + +## **Features Overview** + +* **Dynamic Task Breakdown:** Automatically decompose user requests into smaller tasks with defined dependencies. +* **Task Execution:** Execute tasks such as code modifications, running shell commands, conducting code reviews, and + more. +* **Dependency Management:** Visualize and manage task dependencies using Mermaid diagrams. +* **Asynchronous Processing:** Execute tasks concurrently where possible, respecting dependencies to optimize + performance. +* **Customizable Settings:** Tailor the planning and execution process through configurable settings. +* **Extensible Architecture:** Easily add new task types and extend existing functionalities to fit unique requirements. + +--- + +## **User Guide** + +### **Accessing the Plan Feature** + +To utilize the Plan feature, navigate to the application interface where task planning and management functionalities +are exposed. This could be within a web UI or another integrated platform provided by your organization. + +### **Creating and Managing Plans** + +1. **Initiate a New Plan:** + +* Enter your high-level request or goal into the provided input field. +* For example: "Set up a new React project with TypeScript and implement authentication." + +2. **Task Breakdown:** + +* The system will analyze your request and automatically generate a list of actionable tasks. +* Each task will include details such as task type, description, dependencies, input files, and output files. + +3. **Review Generated Tasks:** + +* Examine the generated tasks to ensure they align with your expectations. +* Modify or adjust tasks if necessary through the interface (if supported). + +4. **Execute the Plan:** + +* Start the execution process, which will carry out each task based on the defined dependencies. +* Monitor the progress through real-time updates and logs. + +### **Understanding Task Dependencies** + +* **Visual Representation:** + * The Plan feature utilizes Mermaid to render task dependency graphs. + * Access the "Task Dependency Graph" section to visualize how tasks interrelate. + +* **Interpreting the Graph:** + * Nodes represent individual tasks. + * Arrows indicate dependencies; a task will only execute after its dependent tasks are completed. + * Styles (e.g., color-coding) indicate the state of each task (Pending, In Progress, Completed). + +### **Viewing Execution Logs** + +* **Accessing Logs:** + * Execution logs are automatically generated and can be accessed through the UI. + * Logs provide detailed information about each task's execution, including API interactions and any errors + encountered. + +* **Log Files:** + * Logs are stored in the `.logs` directory within your working directory. + * Each task execution generates a unique log file for traceability. + +--- + +## **Developer Guide** + +This section provides in-depth information for developers looking to understand, maintain, or extend the Plan feature. + +### **Architecture Overview** + +The Plan feature is built around several core components: + +1. **TaskType:** Defines various types of tasks that can be executed. +2. **PlanSettings:** Configures settings related to task execution, such as models, command environments, and + task-specific configurations. +3. **PlanCoordinator:** Manages the execution of tasks, handling dependencies, threading, and interaction with external + APIs. +4. **PlanUtil:** Contains utility functions for rendering, filtering, and managing task plans. +5. **PlanningTask:** Represents individual planning tasks and manages their execution logic. +6. **AbstractTask:** A base class for all tasks, providing common functionality and state management. + +### **Task Types** + +`TaskType.kt` defines various tasks like `FileModification`, `RunShellCommand`, `CodeReview`, etc. Each task type is +associated with a specific constructor that defines how the task is instantiated and executed. + +##### **1. CommandAutoFixTask** + +* **Description:** + + The `CommandAutoFixTask` is designed to execute specified shell commands and automatically address any issues that + arise during their execution. This task type streamlines the process of running commands by integrating automated + troubleshooting and fixes based on predefined configurations. + +* **Functionality:** + + * **Command Execution:** Runs a user-specified command with provided arguments. + * **Automatic Fixing:** Analyzes the command's output and applies fixes if issues are detected. + * **Configurability:** Allows customization of available commands and auto-fix behavior through `PlanSettings`. + * **Logging and Notifications:** Logs the execution results and notifies users of successes or failures. + +* **Key Components:** + + * **`planSettings.commandAutoFixCommands`:** A list of available commands that can be auto-fixed. + * **`CmdPatchApp`:** Utilized to execute the command and handle patching based on the output. + * **Semaphore Mechanism:** Ensures that the task waits for the command execution and auto-fixing process to complete + before proceeding. + +* **Configuration:** + + * **Available Commands:** Defined in `PlanSettings.kt` under `commandAutoFixCommands`. Developers can add or remove + commands as needed. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic fixing. + +**Implementation Details:** + +The `run` method orchestrates the execution by: + +1. **Identifying the Command:** Matches the alias provided in the task with available commands. +2. **Setting the Working Directory:** Resolves the specified working directory or defaults to the root. +3. **Executing the Command:** Utilizes `CmdPatchApp` to run the command with the specified arguments. +4. **Handling Results:** Based on the exit code, it either applies auto-fixes or notifies the user of failures. + +* **Extending the Task:** + + * **Adding New Commands:** Update `commandAutoFixCommands` in `PlanSettings.kt` with the new command paths. + * **Custom Fix Logic:** Modify the behavior within the `run` method or extend `CmdPatchApp` for specialized fixing + mechanisms. + +--- + +##### **2. InquiryTask** + +* **Description:** + + The `InquiryTask` facilitates answering user-defined questions by reading relevant files and providing comprehensive + summaries. This task type is essential for gathering information, generating insights, and ensuring that responses are + well-informed and contextually accurate. + +* **Functionality:** + + * **Question Handling:** Processes user questions and objectives to generate meaningful answers. + * **Contextual Analysis:** Reads and analyzes input files to provide informed responses. + * **Markdown Rendering:** Formats the output in markdown for easy readability and discussion. + * **Blocking and Asynchronous Execution:** Supports both blocking and non-blocking modes based on configuration. + +* **Key Components:** + + * **`SimpleActor`:** Utilizes a prompt tailored to generate accurate and relevant information based on user inquiries. + * **`Discussable`:** Handles interactive discussions with the user, allowing for response revisions and approvals. + * **Semaphore Mechanism:** Ensures proper synchronization during the inquiry process. + +* **Configuration:** + + * **Enabled Task Types:** Filters which task types are supported during the inquiry to align responses with system + capabilities. + * **Model Selection:** Determines which OpenAI model to use for generating responses, configurable via `PlanSettings`. + +* **Implementation Details:** + + The `run` method executes by: + + 1. **Preparing Input:** Combines user messages, plan details, prior code, and input files into a structured input. + 2. **Generating Response:** Leveraging `inquiryActor` to produce a detailed and formatted answer. + 3. **Handling Blocking Mode:** If enabled, facilitates an interactive discussion allowing user revisions; otherwise, + provides a direct response. + 4. **Updating Task State:** Stores the inquiry result in the processing state for record-keeping and further actions. + +* **Extending the Task:** + + * **Custom Prompts:** Modify the prompt within `SimpleActor` to tailor the inquiry process. + * **Response Handling:** Adjust how responses are processed and displayed by altering the `outputFn` or integrating + additional formatting. + +--- + +##### **3. FileModificationTask** +* **Description:** + + The `FileModificationTask` automates the process of modifying existing files or creating new ones based on specified + requirements and contexts. It ensures that code changes are efficient, maintainable, and adhere to best practices by + generating precise patches or new file structures. + +* **Functionality:** + + * **Patch Generation:** Creates patches for existing files, highlighting additions and deletions. + * **New File Creation:** Generates new files with appropriate structuring and syntax highlighting. + * **Summary Reporting:** Provides a concise summary of all modifications made. + * **Auto-Application of Changes:** Depending on configuration, can automatically apply the generated patches or prompt + for user approval. + +* **Key Components:** + + * **`SimpleActor`:** Uses a detailed prompt to generate accurate file modifications or new file contents. + * **`CmdPatchApp`:** Manages the application of patches to existing files. + * **`addApplyFileDiffLinks`:** Integrates with the UI to allow users to apply or review changes interactively. + +* **Configuration:** + + * **Input and Output Files:** Defines which files are to be examined or created. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to determine whether changes should be applied + automatically. + * **Language Specifications:** For new files, specifies the programming language to ensure correct syntax + highlighting. + +* **Implementation Details:** + + The `run` method operates by: + + 1. **Validating Input Files:** Ensures that there are files specified for modification or creation. + 2. **Generating Code Changes:** Uses `fileModificationActor` to produce the necessary patches or new file contents. + 3. **Applying Changes:** Depending on `autoFix`, either applies changes automatically or provides links for user + approval. + 4. **Logging and Tracking:** Updates the task state with the results of the modifications for future reference. + +* **Extending the Task:** + + * **Custom Modification Logic:** Enhance the prompt within `SimpleActor` to define specific modification behaviors. + * **Integration with Additional Services:** Extend the task to interact with other systems or services as needed. + * **Advanced Patch Handling:** Modify how patches are applied or reviewed by customizing `addApplyFileDiffLinks` and + related methods. + +--- + + +##### **4. RunShellCommandTask** +* **Description:** + The `RunShellCommandTask` is designed to execute specified shell commands within a designated working directory. + It facilitates automation of system commands, ensuring outputs are captured and handled appropriately. +* **Functionality:** + * **Command Execution:** Executes user-specified shell commands with provided arguments in the configured working directory. + * **Output Handling:** Captures and processes the standard output and error streams from command execution. + * **Error Management:** Handles any errors or exceptions that occur during command execution gracefully. + * **Configurability:** Allows customization of environment variables and command behavior through `PlanSettings`. + * **Logging and Notifications:** Logs command outputs and notifies users of command execution statuses. +* **Key Components:** + * **`planSettings.command`:** Defines the shell commands that can be executed. Developers can add custom commands as needed. + * **`ProcessInterpreter`:** Utilized to execute shell commands and manage their execution lifecycle. + * **Semaphore Mechanism:** Ensures synchronization during the execution of shell commands and processing of their outputs. +* **Configuration:** + * **Command Definitions:** Defined in `PlanSettings.kt` under `command`. Developers can specify which commands are available. + * **Working Directory:** Configured via `planTask.execution_task?.workingDir` or defaults to the root directory. + * **Environment Variables:** Set through `planSettings.env` to customize the execution environment. +* **Implementation Details:** + The `run` method orchestrates the execution by: + 1. **Preparing the Environment:** Sets up the working directory and environment variables as specified. + 2. **Executing the Command:** Uses `ProcessInterpreter` to run the specified shell command with provided arguments. + 3. **Handling Outputs:** Captures the output and error streams, processing them for logging and user notifications. + 4. **Managing Execution State:** Updates the task state based on the success or failure of the command execution. +* **Extending the Task:** + * **Adding New Commands:** Update the `command` list in `PlanSettings.kt` with new shell commands. + * **Custom Output Processing:** Modify the `displayFeedback` method to handle outputs differently or integrate with other systems. + * **Advanced Error Handling:** Enhance the `run` method to manage complex error scenarios or integrate with monitoring tools. + +##### **5. RefactorTask** + +* **Description:** + + The `RefactorTask` is designed to analyze existing code and suggest refactoring improvements to enhance code structure, readability, and maintainability. + +* **Functionality:** + + * **Code Analysis:** Examines the provided code files to identify areas for improvement. + * **Refactoring Suggestions:** Provides detailed recommendations for refactoring, including code reorganization, reducing duplication, and applying design patterns where appropriate. + * **Change Application:** Utilizes diff patches to implement the suggested refactoring changes. + * **Logging and Reporting:** Logs the refactoring process and results, providing traceability and auditability. + +* **Key Components:** + + * **`actorName` and `actorPrompt`:** Define the behavior and instructions for the refactoring analysis. + * **`AbstractAnalysisTask`:** Provides the foundational structure for the task execution logic. + * **`CommandPatchApp`:** Handles the application of generated refactoring patches. + * **Semaphore Mechanism:** Ensures synchronization and proper task execution flow. + +* **Configuration:** + + * **Refactoring Focus Areas:** Specify particular aspects to focus on during refactoring, such as modularity or naming conventions. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic application of refactoring suggestions. + +* **Implementation Details:** + + The `run` method orchestrates the refactoring process by: + + 1. **Analyzing Code:** Uses `analysisActor` to generate refactoring suggestions based on the current codebase. + 2. **Generating Patches:** Creates diff patches representing the suggested changes. + 3. **Applying Changes:** Applies patches automatically if `autoFix` is enabled or prompts the user for approval. + 4. **Logging Results:** Updates logs and task states based on the outcome of the refactoring process. + +* **Extending the Task:** + + * **Custom Refactoring Logic:** Modify the prompt to target specific refactoring goals or integrate additional analysis tools. + * **Integration with Development Tools:** Enhance `CommandPatchApp` to interact with other systems or IDEs. + +##### **6. SecurityAuditTask** + +* **Description:** + + The `SecurityAuditTask` performs comprehensive security audits on provided code files, identifying potential vulnerabilities and ensuring compliance with security best practices. + +* **Functionality:** + + * **Vulnerability Detection:** Scans code for potential security issues such as injection attacks, insecure data handling, and authentication flaws. + * **Compliance Checking:** Ensures code adheres to established security standards and guidelines. + * **Recommendations:** Provides specific recommendations to address identified security vulnerabilities. + * **Reporting:** Generates detailed audit reports with actionable insights. + +* **Key Components:** + + * **`actorName` and `actorPrompt`:** Tailor the analysis behavior for security auditing. + * **`AbstractAnalysisTask`:** Provides the foundational structure for the task execution logic. + * **`CommandPatchApp`:** Facilitates the application of security fixes. + * **Semaphore Mechanism:** Ensures synchronization during the audit process. + +* **Configuration:** + + * **Audit Focus Areas:** Specify particular security aspects to prioritize during audits. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic application of security fixes. + +* **Implementation Details:** + + The `run` method manages the security auditing by: + + 1. **Analyzing Code:** Uses `analysisActor` to identify security vulnerabilities in the codebase. + 2. **Generating Fixes:** Creates diff patches to address the identified security issues. + 3. **Applying Changes:** Applies patches automatically if `autoFix` is enabled or prompts the user for approval otherwise. + 4. **Logging Results:** Updates logs and task states based on the outcome of the audit. + +* **Extending the Task:** + + * **Custom Security Rules:** Modify or extend the audit criteria to include additional security checks. + * **Integration with Security Tools:** Enhance `CommandPatchApp` to work with external security scanning or fixing tools. + +##### **7. CodeOptimizationTask** + +* **Description:** + + The `CodeOptimizationTask` analyzes existing code to suggest optimizations that enhance performance, reduce resource consumption, and improve overall code quality. + +* **Functionality:** + + * **Performance Analysis:** Identifies performance bottlenecks and inefficient code patterns. + * **Optimization Suggestions:** Recommends specific code changes to improve efficiency and performance. + * **Code Refactoring:** Applies optimized changes using diff patches. + * **Impact Assessment:** Evaluates the potential benefits and trade-offs of suggested optimizations. + +* **Key Components:** + + * **`actorName` and `actorPrompt`:** Define the behavior and instructions for code optimization analysis. + * **`AbstractAnalysisTask`:** Provides the foundational structure for the task execution logic. + * **`CommandPatchApp`:** Facilitates the application of optimization changes. + * **Semaphore Mechanism:** Manages synchronization during the optimization process. + +* **Configuration:** + + * **Optimization Targets:** Define areas of focus, such as memory usage, execution speed, or algorithm efficiency. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic application of optimizations. + +* **Implementation Details:** + + The `run` method orchestrates the optimization process by: + + 1. **Analyzing Code:** Utilizes `analysisActor` to identify optimization opportunities within the codebase. + 2. **Generating Patches:** Creates diff patches representing the suggested optimizations. + 3. **Applying Changes:** Applies patches automatically if `autoFix` is enabled or prompts the user for approval. + 4. **Logging Results:** Updates logs and task states based on the outcome of the optimization process. + +* **Extending the Task:** + + * **Custom Optimization Criteria:** Modify the prompt to target specific optimization goals or integrate performance profiling tools. + * **Advanced Patch Handling:** Enhance how patches are applied to support more complex optimization scenarios. + +##### **8. CodeReviewTask** + +* **Description:** + + The `CodeReviewTask` conducts thorough code reviews on provided code files, assessing code quality, adherence to standards, and identifying potential issues. + +* **Functionality:** + + * **Quality Assessment:** Evaluates code for readability, maintainability, and adherence to coding standards. + * **Issue Identification:** Detects potential bugs, logical errors, and performance issues within the codebase. + * **Best Practices Enforcement:** Ensures that code follows industry best practices and organizational guidelines. + * **Recommendations:** Provides actionable suggestions for improving code quality and resolving identified issues. + +* **Key Components:** + + * **`actorName` and `actorPrompt`:** Tailor the analysis behavior for code reviews. + * **`AbstractAnalysisTask`:** Provides the foundational structure for the task execution logic. + * **`CommandPatchApp`:** Facilitates the application of code review suggestions. + * **Semaphore Mechanism:** Ensures synchronization during the review process. + +* **Configuration:** + + * **Review Scope:** Define which aspects of the code (e.g., security, performance) to prioritize during the review. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic application of review suggestions. + +* **Implementation Details:** + + The `run` method manages the code review by: + + 1. **Analyzing Code:** Uses `analysisActor` to assess code quality and identify issues. + 2. **Generating Suggestions:** Creates diff patches with recommended code changes to improve quality. + 3. **Applying Changes:** Applies patches automatically if `autoFix` is enabled or prompts the user for approval. + 4. **Logging Results:** Updates logs and task states based on the outcome of the code review. + +* **Extending the Task:** + + * **Custom Review Criteria:** Modify the prompt to focus on specific review aspects or integrate additional analysis tools. + * **Integration with CI/CD Pipelines:** Enhance `CommandPatchApp` to work seamlessly with continuous integration systems. + +##### **9. DocumentationTask** + +* **Description:** + + The `DocumentationTask` generates comprehensive documentation for provided code files, ensuring clarity, completeness, and ease of understanding for future developers. + +* **Functionality:** + + * **Automatic Documentation Generation:** Creates detailed documentation covering code purpose, functionality, inputs, outputs, and usage examples. + * **Structured Formatting:** Ensures that documentation is organized in a consistent and readable format. + * **Code Examples:** Includes code snippets to illustrate usage and functionality. + * **Update Tracking:** Monitors changes in code to automatically update relevant documentation sections. + +* **Key Components:** + + * **`documentationGeneratorActor`:** Generates the documentation content based on provided code files. + * **`MarkdownUtil`:** Formats the generated documentation into markdown for consistency and readability. + * **Semaphore Mechanism:** Manages synchronization during the documentation generation process. + +* **Configuration:** + + * **Documentation Scope:** Define which aspects of the code to document, such as functions, classes, and modules. + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic acceptance of generated documentation. + +* **Implementation Details:** + + The `run` method orchestrates the documentation generation by: + + 1. **Generating Documentation:** Utilizes `documentationGeneratorActor` to create detailed documentation based on the codebase. + 2. **Formatting Output:** Formats the documentation using `MarkdownUtil` for integration into the UI. + 3. **Applying or Approving Documentation:** Automatically accepts the documentation if `autoFix` is enabled, or provides an interface for user approval. + 4. **Logging Results:** Updates task states and logs based on the outcome of the documentation process. + +* **Extending the Task:** + + * **Custom Documentation Templates:** Modify the prompt to generate documentation in specific formats or styles. + * **Integration with Documentation Systems:** Enhance the task to integrate with existing documentation platforms or tools. + +##### **10. TestGenerationTask** + +* **Description:** + + The `TestGenerationTask` generates comprehensive unit tests for provided code files, ensuring functionality correctness and high code coverage. + +* **Functionality:** + + * **Unit Test Creation:** Produces unit tests covering all public methods and functions, including positive, negative, and edge cases. + * **Framework Integration:** Utilizes appropriate testing frameworks and assertion libraries based on the target language. + * **Setup and Teardown:** Includes necessary setup and teardown methods to prepare the testing environment. + * **Commenting:** Adds comments explaining the purpose and functionality of each test case. + +* **Key Components:** + + * **`actorName` and `actorPrompt`:** Define the behavior and instructions for test generation analysis. + * **`AbstractAnalysisTask`:** Provides the foundational structure for the task execution logic. + * **`CommandPatchApp`:** Facilitates the creation and application of test files. + * **Semaphore Mechanism:** Manages synchronization during the test generation process. + +* **Configuration:** + + * **Test Coverage Targets:** Define the desired code coverage levels and test case types (e.g., boundary cases). + * **Auto-Fix Toggle:** Controlled via `planSettings.autoFix` to enable or disable automatic creation of test files. + +* **Implementation Details:** + + The `run` method manages the test generation process by: + + 1. **Generating Tests:** Utilizes `analysisActor` to generate unit tests based on the provided code files. + 2. **Creating Test Files:** Formats and structures generated tests into runnable code files using appropriate testing frameworks. + 3. **Applying Tests:** Automatically creates test files if `autoFix` is enabled or prompts the user for approval. + 4. **Logging Results:** Updates logs and task states based on the outcome of the test generation process. + +* **Extending the Task:** + + * **Custom Test Case Logic:** Modify the prompt to generate specific types of tests or integrate with additional testing tools. + * **Integration with CI/CD Pipelines:** Enhance `CommandPatchApp` to work with continuous integration systems and testing frameworks. + +#### **Adding New Task Types** + +To introduce a new task type: + +1. **Define the Task Type:** + ```kotlin + val NewTaskType = TaskType("NewTaskType") + ``` + +2. **Create the Task Class:** + +* Extend `AbstractTask` and implement necessary methods. + + ```kotlin + class NewTaskTypeTask(settings: PlanSettings, task: PlanningTask.PlanTask) : AbstractTask(settings, task) { + override fun promptSegment(): String { + return "Description for NewTaskType" + } + + override fun run( + agent: PlanCoordinator, + taskId: String, + userMessage: String, + plan: TaskBreakdownInterface, + planProcessingState: PlanProcessingState, + task: SessionTask, + api: API + ) { + // Implementation for task execution + } + } + ``` + +3. **Register the Task Type:** + +* In `TaskType.kt`, within the `init` block, register the constructor. + + ```kotlin + registerConstructor(NewTaskType) { settings, task -> NewTaskTypeTask(settings, task) } + ``` + +4. **Update PlanSettings:** + +* Ensure that the new task type is configurable within `PlanSettings` if necessary. + +### **Configuring Plan Settings** + +`PlanSettings.kt` allows for extensive configuration of how tasks are planned and executed. + +* **Default and Parsing Models:** + * Define which OpenAI models to use for default operations and parsing tasks. + +* **Command Environment:** + * Configure the command-line environment, such as using `bash` or `powershell` based on the operating system. + +* **Temperature and Budget:** + * Adjust the creativity (`temperature`) and resource allocation (`budget`) for task planning. + +* **Task-Specific Settings:** + * Enable or disable specific task types and assign models to them. + * Example: + ```kotlin + val taskSettings: MutableMap = TaskType.values().associateWith { taskType -> + TaskSettings( + when (taskType) { + TaskType.FileModification, TaskType.Inquiry -> true + else -> false + } + ) + }.toMutableMap() + ``` + +* **Auto-Fix and Blocking Behavior:** + * Configure whether the system should attempt to auto-fix issues and whether to allow blocking operations. + +### **Extending PlanCoordinator** + +`PlanCoordinator.kt` is pivotal in managing the lifecycle of plans and their tasks. + +* **Initialization:** + * Handles the initial breakdown of tasks based on user input. + * Utilizes `PlanUtil` for filtering and organizing tasks. + +* **Task Execution:** + * Manages threading and asynchronous task execution using `ThreadPoolExecutor`. + * Handles dependencies to ensure tasks execute in the correct order. + +* **Logging and Error Handling:** + * Integrates logging for monitoring task executions. + * Captures and logs errors to assist in debugging. + +**Extending Functionality:** + +* **Custom Execution Strategies:** + * Modify how tasks are queued and executed by overriding methods in `PlanCoordinator`. + +* **Enhancing Logging:** + * Implement additional logging mechanisms or integrate with external monitoring systems. + +### **Utilizing PlanUtil** + +`PlanUtil.kt` provides utility functions essential for task management and visualization. + +* **Diagram Generation:** + * Uses Mermaid to create visual representations of task dependencies. + +* **Rendering Plans:** + * Provides functions to render plans in various formats (Text, JSON, Diagram). + +* **Plan Filtering:** + * Filters out invalid or circular dependencies in task plans. + +**Custom Utilities:** + +* **Extend `PlanUtil` with Additional Helpers:** + * Implement new utility functions as needed to support extended features. + +### **Handling Asynchronous Task Execution** + +The system leverages a `ThreadPoolExecutor` to manage task execution asynchronously. + +* **Concurrency Management:** + * Configure the thread pool size and behavior based on system capabilities and workload. + +* **Task Dependencies:** + * Ensure tasks wait for their dependencies to complete before execution. + +* **Error Propagation:** + * Implement mechanisms to handle exceptions in asynchronous tasks gracefully. + +### **Customizing Task Execution** + +Developers can customize how individual tasks execute by: + +1. **Overriding the `run` Method:** + +* Implement specific logic for new or existing tasks by overriding the `run` method in task classes. + +2. **Integrating with External Systems:** + +* Extend task execution to interact with databases, APIs, or other services as required. + +3. **Enhancing the Execution Flow:** + +* Modify the execution flow in `PlanCoordinator` to support complex scenarios or additional dependencies. + +--- + +## **Best Practices** + +* **Modular Design:** + * Keep task implementations modular to facilitate easy maintenance and extension. + +* **Robust Error Handling:** + * Implement comprehensive error handling within tasks to ensure the system remains stable. + +* **Efficient Dependency Management:** + * Clearly define task dependencies to avoid circular dependencies and ensure smooth execution. + +* **Logging and Monitoring:** + * Maintain detailed logs for all task executions to aid in monitoring and troubleshooting. + +* **Security Considerations:** + * Ensure that executing shell commands or modifying files does not introduce security vulnerabilities. + +--- + +## **Troubleshooting** + +1. **Circular Dependency Detected:** + +* **Issue:** The system throws a "Circular dependency detected in task breakdown" error. +* **Solution:** Review the task definitions to ensure that dependencies are acyclic. Modify task dependencies to + eliminate cycles. + +2. **Unknown Task Type Error:** + +* **Issue:** An error stating "Unknown task type" is encountered. +* **Solution:** Ensure that all custom task types are properly defined and registered in `TaskType.kt`. + +3. **API Errors:** + +* **Issue:** Failures in API interactions during task execution. +* **Solution:** Check API credentials, network connectivity, and API service status. Review logs for detailed error + messages. + +4. **File Reading Errors:** + +* **Issue:** Errors while reading input or output files. +* **Solution:** Verify file paths, permissions, and the existence of the specified files. + +5. **Task Execution Failures:** + +* **Issue:** Individual tasks fail during execution. +* **Solution:** Examine the logs associated with the failed task for error details. Ensure that task configurations are + correct and dependencies are met. + +--- + +## **Frequently Asked Questions (FAQs)** + +**Q1: How do I enable or disable specific task types?** + +* **A:** Modify the `taskSettings` in `PlanSettings.kt` by setting the `enabled` property for each `TaskType`. + +**Q2: Can I add custom commands for shell tasks?** + +* **A:** Yes. When defining a new task type or configuring existing ones, specify the custom commands in the + `ExecutionTask` data class. + +**Q3: How are task dependencies managed?** + +* **A:** Dependencies are defined in the `task_dependencies` field of each `PlanTask`. The system ensures that dependent + tasks execute only after their dependencies have been completed. + +**Q4: Is it possible to log API interactions for auditing purposes?** + +* **A:** Yes. The `PlanCoordinator` creates log files for each API interaction, stored in the `.logs` directory. + +**Q5: How can I visualize the task execution flow?** + +* **A:** The Plan feature generates Mermaid diagrams that visually represent task dependencies and execution flow, + accessible within the application interface. \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt index 49ad53c4..7d152de5 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/webui/session/SocketManagerBase.kt @@ -81,7 +81,7 @@ abstract class SocketManagerBase( } override fun createFile(relativePath: String): Pair { log.debug("Saving file at path: {}", relativePath) - return Pair("fileIndex/$session/$relativePath", dataStorage?.getSessionDir(owner, session)?.let { dir -> + return Pair("fileIndex/$session/$relativePath", dataStorage?.getDataDir(owner, session)?.let { dir -> dir.mkdirs() val resolve = dir.resolve(relativePath) resolve.parentFile.mkdirs() @@ -110,7 +110,7 @@ abstract class SocketManagerBase( try { val ver = messageVersions[messageID]?.get() val v = messageStates[messageID] - log.info("Publish Msg: {} - {} - {} - {} bytes", session, messageID, ver, v?.length) + log.debug("Publish Msg: {} - {} - {} - {} bytes", session, messageID, ver, v?.length) sockets.keys.toTypedArray().forEach { chatSocket -> try { val deque = sendQueues.computeIfAbsent(chatSocket) { ConcurrentLinkedDeque() } @@ -125,7 +125,7 @@ abstract class SocketManagerBase( val v = messageStates[messageID] msg = "$messageID,$ver,$v" } finally { - log.info("Sending message: {} to socket: {}", msg, chatSocket) + log.debug("Sending message: {} to socket: {}", msg, chatSocket) synchronized(chatSocket) { chatSocket.remote.sendString(msg) } diff --git a/webui/src/main/resources/application/functions.js b/webui/src/main/resources/application/functions.js index d36477db..fb9656c4 100644 --- a/webui/src/main/resources/application/functions.js +++ b/webui/src/main/resources/application/functions.js @@ -7,9 +7,12 @@ export async function fetchData(endpoint, useSession = true) { endpoint = endpoint + "?sessionId=" + sessionId; } } - const modalContent = document.getElementById('modal-content'); + const modalContent = getCachedElement('modal-content'); if (modalContent) modalContent.innerHTML = "
Loading...
"; const response = await fetch(endpoint); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } const text = await response.text(); if (modalContent) modalContent.innerHTML = "
" + text + "
"; if (typeof Prism !== 'undefined') { @@ -17,37 +20,56 @@ export async function fetchData(endpoint, useSession = true) { } } catch (error) { console.error('Error fetching data:', error); + const modalContent = document.getElementById('modal-content'); + if (modalContent) modalContent.innerHTML = "
Error loading content. Please try again later.
"; + throw error; // Re-throw the error for the caller to handle if needed } } export function getSessionId() { if (!window.location.hash) { - fetch('newSession') + return fetch('newSession') .then(response => { if (response.ok) { return response.text(); } else { - throw new Error('Failed to get new session ID'); + throw new Error(`Failed to get new session ID. Status: ${response.status}`); } }) .then(sessionId => { window.location.hash = sessionId; - connect(sessionId); + return sessionId; + }) + .catch(error => { + console.error('Error getting session ID:', error.message); + throw error; // Re-throw the error for the caller to handle }); } else { return window.location.hash.substring(1); } } +const elementCache = new Map(); + +export function getCachedElement(id) { + if (!elementCache.has(id)) { + const element = document.getElementById(id); + if (element) { + elementCache.set(id, element); + } + } + return elementCache.get(id); +} + export function showModal(endpoint, useSession = true) { fetchData(endpoint, useSession).then(r => { - const modal = document.getElementById('modal'); + const modal = getCachedElement('modal'); if (modal) modal.style.display = 'block'; }); } export function closeModal() { - const modal = document.getElementById('modal'); + const modal = getCachedElement('modal'); if (modal) modal.style.display = 'none'; } @@ -147,15 +169,15 @@ export function closeModal() { export function toggleVerbose() { - let verboseToggle = document.getElementById('verbose'); + let verboseToggle = getCachedElement('verbose'); if (verboseToggle.innerText === 'Hide Verbose') { - const elements = document.getElementsByClassName('verbose'); + const elements = Array.from(document.getElementsByClassName('verbose')); for (let i = 0; i < elements.length; i++) { elements[i].classList.add('verbose-hidden'); // Add the 'verbose-hidden' class to hide } verboseToggle.innerText = 'Show Verbose'; } else if (verboseToggle.innerText === 'Show Verbose') { - const elements = document.getElementsByClassName('verbose'); + const elements = Array.from(document.getElementsByClassName('verbose')); for (let i = 0; i < elements.length; i++) { elements[i].classList.remove('verbose-hidden'); // Remove the 'verbose-hidden' class to show } @@ -166,13 +188,13 @@ export function toggleVerbose() { } export function refreshReplyForms() { - document.querySelectorAll('.reply-input').forEach(messageInput => { + Array.from(document.getElementsByClassName('reply-input')).forEach(messageInput => { messageInput.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); let form = messageInput.closest('form'); if (form) { - let textSubmitButton = form.querySelector('.text-submit-button'); + let textSubmitButton = form.getElementsByClassName('text-submit-button')[0]; if (textSubmitButton) { textSubmitButton.click(); } else { @@ -186,14 +208,14 @@ export function refreshReplyForms() { export function refreshVerbose() { - let verboseToggle = document.getElementById('verbose'); + let verboseToggle = getCachedElement('verbose'); if (verboseToggle.innerText === 'Hide Verbose') { - const elements = document.getElementsByClassName('verbose'); + const elements = Array.from(document.getElementsByClassName('verbose')); for (let i = 0; i < elements.length; i++) { elements[i].classList.remove('verbose-hidden'); // Remove the 'verbose-hidden' class to show } } else if (verboseToggle.innerText === 'Show Verbose') { - const elements = document.getElementsByClassName('verbose'); + const elements = Array.from(document.getElementsByClassName('verbose')); for (let i = 0; i < elements.length; i++) { elements[i].classList.add('verbose-hidden'); // Add the 'verbose-hidden' class to hide } @@ -211,9 +233,9 @@ export function findAncestor(element, selector) { export function applyToAllSvg() { - document.querySelectorAll('svg').forEach(svg => { + Array.from(document.getElementsByTagName('svg')).forEach(svg => { if (!svg.dataset.svgPanZoomInitialized) { new SvgPanZoom().init(svg); } }); -} +} \ No newline at end of file diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js index d432c51c..cc16dee3 100644 --- a/webui/src/main/resources/application/main.js +++ b/webui/src/main/resources/application/main.js @@ -1,5 +1,5 @@ import {connect} from './chat.js'; -import {applyToAllSvg, getSessionId, refreshReplyForms, refreshVerbose} from './functions.js'; +import {getCachedElement, getSessionId, refreshReplyForms, refreshVerbose} from './functions.js'; import {updateTabs} from './tabs.js'; import {setupUIHandlers} from './uiHandlers.js'; import {onWebSocketText} from './messageHandling.js'; @@ -43,7 +43,7 @@ const updateDocumentComponents = debounce(function () { } try { if (typeof mermaid !== 'undefined') { - const mermaidDiagrams = document.querySelectorAll('.mermaid:not(.mermaid-processed)'); + const mermaidDiagrams = Array.from(document.getElementsByClassName('mermaid')).filter(el => !el.classList.contains('mermaid-processed')); if (mermaidDiagrams.length > 0) { mermaid.run(); mermaidDiagrams.forEach(diagram => diagram.classList.add('mermaid-processed')); @@ -66,15 +66,15 @@ document.addEventListener('DOMContentLoaded', () => { updateTabs(); setupUIHandlers(); - const loginLink = document.getElementById('login'); - const usernameLink = document.getElementById('username'); - const userSettingsLink = document.getElementById('user-settings'); - const userUsageLink = document.getElementById('user-usage'); - const logoutLink = document.getElementById('logout'); - const form = document.getElementById('main-input'); - const messageInput = document.getElementById('chat-input'); + const loginLink = getCachedElement('login'); + const usernameLink = getCachedElement('username'); + const userSettingsLink = getCachedElement('user-settings'); + const userUsageLink = getCachedElement('user-usage'); + const logoutLink = getCachedElement('logout'); + const form = getCachedElement('main-input'); + const messageInput = getCachedElement('chat-input'); const sessionId = getSessionId(); - const messages = document.getElementById('messages'); + const messages = getCachedElement('messages'); if (sessionId) { console.log(`Connecting with session ID: ${sessionId}`); @@ -99,11 +99,11 @@ document.addEventListener('DOMContentLoaded', () => { updateTabs(); - document.querySelectorAll('.tabs-container').forEach(tabsContainer => { + Array.from(document.getElementsByClassName('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}"]`); + const savedButton = tabsContainer.querySelector(`[data-for-tab="${savedTab}"]`); console.log('Main script finished loading'); if (savedButton) { savedButton.click(); diff --git a/webui/src/main/resources/application/tabs.js b/webui/src/main/resources/application/tabs.js index 92b72cb6..ebdf80f9 100644 --- a/webui/src/main/resources/application/tabs.js +++ b/webui/src/main/resources/application/tabs.js @@ -4,6 +4,29 @@ const tabCache = new Map(); let isRestoringTabs = false; const MAX_RECURSION_DEPTH = 10; const OPERATION_TIMEOUT = 5000; // 5 seconds +function setActiveTab(button, tabsContainer) { + const forTab = button.getAttribute('data-for-tab'); + const tabsContainerId = tabsContainer.id; + if (button.classList.contains('active')) return; + try { + localStorage.setItem(`selectedTab_${tabsContainerId}`, forTab); + tabCache.set(tabsContainerId, forTab); + } catch (e) { + console.warn('Failed to save tab state to localStorage:', e); + } + tabsContainer.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + tabsContainer.querySelectorAll('.tab-content').forEach(content => { + if (content.getAttribute('data-tab') === forTab) { + content.classList.add('active'); + content.style.display = 'block'; + updateNestedTabs(content, 0); + } else { + content.classList.remove('active'); + content.style.display = 'none'; + } + }); +} export function updateTabs() { const tabButtons = document.querySelectorAll('.tab-button'); @@ -11,33 +34,8 @@ export function updateTabs() { const clickHandler = (event) => { event.stopPropagation(); const button = event.currentTarget; - const forTab = button.getAttribute('data-for-tab'); const tabsContainer = button.closest('.tabs-container'); - const tabsContainerId = tabsContainer.id; - if (button.classList.contains('active')) return; - try { - localStorage.setItem(`selectedTab_${tabsContainerId}`, forTab); - tabCache.set(tabsContainerId, forTab); - } catch (e) { - console.warn('Failed to save tab state to localStorage:', e); - } - const buttons = tabsContainer.querySelectorAll('.tab-button'); - for (let i = 0; i < buttons.length; i++) { - buttons[i].classList.remove('active'); - } - button.classList.add('active'); - const contents = tabsContainer.querySelectorAll('.tab-content'); - for (let i = 0; i < contents.length; i++) { - const content = contents[i]; - if (content.getAttribute('data-tab') === forTab) { - content.classList.add('active'); - content.style.display = 'block'; - updateNestedTabs(content, 0); - } else { - content.classList.remove('active'); - content.style.display = 'none'; - } - } + setActiveTab(button, tabsContainer); }; tabButtons.forEach(button => { diff --git a/webui/src/main/resources/application/uiHandlers.js b/webui/src/main/resources/application/uiHandlers.js index 4128230c..076c1647 100644 --- a/webui/src/main/resources/application/uiHandlers.js +++ b/webui/src/main/resources/application/uiHandlers.js @@ -1,25 +1,23 @@ import {closeModal, findAncestor, getSessionId, showModal, toggleVerbose} from './functions.js'; -import {queueMessage} from './chat.js'; + function isCtrlOrCmd(event) { return event.ctrlKey || (navigator.platform.toLowerCase().indexOf('mac') !== -1 && event.metaKey); } +function addClickHandler(elementId, action) { + const element = document.getElementById(elementId); + if (element) element.addEventListener('click', action); +} + export function setupUIHandlers() { - const historyElement = document.getElementById('history'); - if (historyElement) historyElement.addEventListener('click', () => showModal('sessions')); - const settingsElement = document.getElementById('settings'); - if (settingsElement) settingsElement.addEventListener('click', () => showModal('settings')); - const usageElement = document.getElementById('usage'); - if (usageElement) usageElement.addEventListener('click', () => showModal('usage')); - const verboseElement = document.getElementById('verbose'); - if (verboseElement) verboseElement.addEventListener('click', () => toggleVerbose()); - const deleteElement = document.getElementById('delete'); - if (deleteElement) deleteElement.addEventListener('click', () => showModal('delete')); - const cancelElement = document.getElementById('cancel'); - if (cancelElement) cancelElement.addEventListener('click', () => showModal('cancel')); - const threadsElement = document.getElementById('threads'); - if (threadsElement) threadsElement.addEventListener('click', () => showModal('threads')); + addClickHandler('history', () => showModal('sessions')); + addClickHandler('settings', () => showModal('settings')); + addClickHandler('usage', () => showModal('usage')); + addClickHandler('verbose', toggleVerbose); + addClickHandler('delete', () => showModal('delete')); + addClickHandler('cancel', () => showModal('cancel')); + addClickHandler('threads', () => showModal('threads')); const shareElement = document.getElementById('share'); if (shareElement) { shareElement.addEventListener('click', () => showModal('share?url=' + encodeURIComponent(window.location.href) + "&loadImages=true", false)); @@ -55,25 +53,25 @@ function handleBodyClick(event) { if (hrefLink) { const messageId = hrefLink.getAttribute('data-id'); console.log('Href link clicked, messageId:', messageId); - if (messageId && messageId !== '' && messageId !== null) queueMessage('!' + messageId + ',link'); + handleMessageAction(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'); + handleMessageAction(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'); + handleMessageAction(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'); + handleMessageAction(messageId, 'stop'); } else { const textSubmitButton = findAncestor(target, '.text-submit-button'); if (textSubmitButton) { @@ -81,7 +79,7 @@ function handleBodyClick(event) { console.log('Text submit button clicked, messageId:', messageId); const text = document.querySelector('.reply-input[data-id="' + messageId + '"]').value; const escapedText = encodeURIComponent(text); - if (messageId && messageId !== '' && messageId !== null) queueMessage('!' + messageId + ',userTxt,' + escapedText); + handleMessageAction(messageId, `userTxt,${escapedText}`); } } } diff --git a/webui/src/main/resources/application/uiSetup.js b/webui/src/main/resources/application/uiSetup.js index 42c586b1..c43ef8d3 100644 --- a/webui/src/main/resources/application/uiSetup.js +++ b/webui/src/main/resources/application/uiSetup.js @@ -1,6 +1,21 @@ import {showModal} from './functions.js'; import {queueMessage} from './chat.js'; +function adjustTextareaHeight(textarea, maxLines) { + textarea.style.height = 'auto'; + textarea.style.height = (textarea.scrollHeight) + 'px'; + const computedStyle = window.getComputedStyle(textarea); + let lineHeight = computedStyle.lineHeight === 'normal' + ? parseInt(computedStyle.fontSize) * 1.2 + : parseInt(computedStyle.lineHeight); + if (textarea.scrollHeight > lineHeight * maxLines) { + textarea.style.height = (lineHeight * maxLines) + 'px'; + textarea.style.overflowY = 'scroll'; + } else { + textarea.style.overflowY = 'hidden'; + } +} + export function setupMessageInput(form, messageInput) { if (messageInput) { messageInput.addEventListener('keydown', (event) => { @@ -17,24 +32,9 @@ export function setupMessageInput(form, messageInput) { messageInput.style.height = ''; messageInput.addEventListener('input', function () { console.log('Input event on message input'); - this.style.height = 'auto'; - this.style.height = (this.scrollHeight - heightAdjustment) + 'px'; - const computedStyle = window.getComputedStyle(this); - let lineHeight = computedStyle.lineHeight; - if (lineHeight === 'normal') { - lineHeight = parseInt(computedStyle.fontSize) * 1.2; - } else { - lineHeight = parseInt(lineHeight); - } - const maxLines = 20; - if (this.scrollHeight > lineHeight * maxLines) { - this.style.height = (lineHeight * maxLines) + 'px'; - this.style.overflowY = 'scroll'; // Enable vertical scrolling - } else { - this.style.overflowY = 'hidden'; // Hide the scrollbar when not needed - } + adjustTextareaHeight(this, 20); }); messageInput.focus(); } 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 60c08f1c..bf4397a7 100644 --- a/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt +++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/webui/ActorTestAppServer.kt @@ -1,6 +1,6 @@ package com.simiacryptus.skyenet.webui -import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAIModels import com.simiacryptus.jopenai.util.ClientUtil.keyTxt import com.simiacryptus.skyenet.apps.general.DocumentParserApp import com.simiacryptus.skyenet.apps.general.PlanAheadApp @@ -54,7 +54,7 @@ object ActorTestAppServer : com.simiacryptus.skyenet.webui.application.Applicati SimpleActor( "Translate the user's request into pig latin.", "PigLatin", - model = ChatModels.GPT35Turbo + model = OpenAIModels.GPT4oMini ) ) ), @@ -63,37 +63,35 @@ object ActorTestAppServer : com.simiacryptus.skyenet.webui.application.Applicati ParsedActor( resultClass = TestJokeDataStructure::class.java, prompt = "Tell me a joke", - parsingModel = ChatModels.GPT35Turbo, - model = ChatModels.GPT35Turbo, + parsingModel = OpenAIModels.GPT4oMini, + model = OpenAIModels.GPT4oMini, ) ) ), - ChildWebApp("/images", ImageActorTestApp(ImageActor(textModel = ChatModels.GPT35Turbo))), + ChildWebApp("/images", ImageActorTestApp(ImageActor(textModel = OpenAIModels.GPT4oMini))), ChildWebApp( "/test_coding_scala", - CodingActorTestApp(CodingActor(ScalaLocalInterpreter::class, model = ChatModels.GPT35Turbo)) + CodingActorTestApp(CodingActor(ScalaLocalInterpreter::class, model = OpenAIModels.GPT4oMini)) ), ChildWebApp( "/test_coding_kotlin", - CodingActorTestApp(CodingActor(KotlinInterpreter::class, model = ChatModels.GPT35Turbo)) + CodingActorTestApp(CodingActor(KotlinInterpreter::class, model = OpenAIModels.GPT4oMini)) ), ChildWebApp( "/test_coding_groovy", - CodingActorTestApp(CodingActor(GroovyInterpreter::class, model = ChatModels.GPT35Turbo)) + CodingActorTestApp(CodingActor(GroovyInterpreter::class, model = OpenAIModels.GPT4oMini)) ), ChildWebApp("/test_file_patch", FilePatchTestApp()), ChildWebApp( "/taskDev", PlanAheadApp( - rootFile = File("."), planSettings = PlanSettings( - model = ChatModels.GPT4o, - parsingModel = ChatModels.GPT4oMini, + defaultModel = OpenAIModels.GPT4o, + parsingModel = OpenAIModels.GPT4oMini, command = listOf("task"), temperature = 0.2, budget = 2.0, autoFix = true, - enableCommandAutoFix = true, commandAutoFixCommands = listOf( "C:\\Program Files\\nodejs\\npx.cmd", "C:\\Program Files\\nodejs\\npm.cmd" ), @@ -101,11 +99,15 @@ object ActorTestAppServer : com.simiacryptus.skyenet.webui.application.Applicati workingDir = ".", language = if (isWindows) "powershell" else "bash", ).apply { - setTaskSettings(TaskType.TaskPlanning, TaskSettings(enabled = true)) - setTaskSettings(TaskType.RunShellCommand, TaskSettings(enabled = false)) + setTaskSettings(TaskType.TaskPlanning, TaskSettings( + enabled = true, + model = OpenAIModels.GPT4o,)) + setTaskSettings(TaskType.RunShellCommand, TaskSettings( + enabled = false, + model = OpenAIModels.GPT4o,)) }, - model = ChatModels.GPT4o, - parsingModel = ChatModels.GPT4oMini, + model = OpenAIModels.GPT4o, + parsingModel = OpenAIModels.GPT4oMini, ) ), ChildWebApp("/stressTest", StressTestApp()), diff --git a/webui/src/test/resources/logback.xml b/webui/src/test/resources/logback.xml index e9b764e2..16803762 100644 --- a/webui/src/test/resources/logback.xml +++ b/webui/src/test/resources/logback.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file