diff --git a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FullReplacementProcessor.kt b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FullReplacementProcessor.kt index ff47af7d4..17c7d7fe5 100644 --- a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FullReplacementProcessor.kt +++ b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FullReplacementProcessor.kt @@ -6,8 +6,8 @@ import com.simiacryptus.cognotik.util.LoggerFactory * A processor that handles full file replacement instead of patching. * This is useful when changes are extensive or when patching would be more complex. */ -class FullReplacementProcessor : PatchProcessor { - override val label: String = "Full Replacement" + class FullReplacementProcessor : PatchProcessor { + override val label = "FullReplacement" override val patchFormatPrompt = """ Response should provide the complete updated file content within ```code blocks. @@ -24,7 +24,7 @@ class FullReplacementProcessor : PatchProcessor { const b = 2; function exampleFunction() { - return b + 2; + return a + b; } module.exports = { exampleFunction }; @@ -42,6 +42,17 @@ class FullReplacementProcessor : PatchProcessor { }); ``` """.trimIndent() + override fun getInitiatorPattern(): Regex { + return "(?s)```\\w*\n".toRegex() + } + override fun extractCodeBlocks(response: String): List> { + val codeblockPattern = """(?s)(? + val language = match.groupValues[1] + val code = match.groupValues[2].trim() + language to code + }.toList() + } override fun generatePatch(oldCode: String, newCode: String): String { log.debug("Generating full replacement patch") diff --git a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FuzzyPatchMatcher.kt b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FuzzyPatchMatcher.kt index 0ea52929e..3fb4135a4 100644 --- a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FuzzyPatchMatcher.kt +++ b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/FuzzyPatchMatcher.kt @@ -43,7 +43,7 @@ open class FuzzyPatchMatcher( private val requireAnchorMatch: Boolean = true, ) : PatchProcessor { /** A descriptive label for this patch processor. */ - override val label: String = "Fuzzy Patch Matcher" +override val label: String = "Fuzzy Patch Matcher" /** * A detailed instructional prompt intended for a language model (LLM). @@ -88,6 +88,26 @@ open class FuzzyPatchMatcher( Alternately, the patch can be provided as a snippet of updated code with context. This is useful when the patch is small and can be applied directly, when creating the delete lines is cumbersome, or when creating a new file. """.trimIndent() + override fun getInitiatorPattern(): Regex { + return "(?s)```\\w*\n".toRegex() + } + override fun extractCodeBlocks(response: String): List> { + val codeblockPattern = """(?s)(? + val language = match.groupValues[1] + val code = match.groupValues[2].trim() + language to code + } + } /** * Generates a diff patch that transforms the `oldCode` into the `newCode`. diff --git a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessor.kt b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessor.kt index c6f7429fe..206ef23dc 100644 --- a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessor.kt +++ b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessor.kt @@ -1,8 +1,18 @@ package com.simiacryptus.cognotik.diff -interface PatchProcessor { + interface PatchProcessor { val label: String val patchFormatPrompt: String fun generatePatch(oldCode: String, newCode: String): String fun applyPatch(source: String, patch: String): String + /** + * Extracts code blocks from markdown-formatted text + * @param response The markdown text containing code blocks + * @return List of pairs containing (language, code content) + */ + fun extractCodeBlocks(response: String): List> + /** + * Gets the regex pattern that initiates a code block + */ + fun getInitiatorPattern(): Regex } \ No newline at end of file diff --git a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessors.kt b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessors.kt index 2552ad312..c13bdce61 100644 --- a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessors.kt +++ b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/PatchProcessors.kt @@ -5,6 +5,8 @@ enum class PatchProcessors : PatchProcessor { FullReplacement { override val label = "FullReplacement" override val matcher = FullReplacementProcessor() + override fun extractCodeBlocks(response: String) = matcher.extractCodeBlocks(response) + override fun getInitiatorPattern() = matcher.getInitiatorPattern() }, // Thermodynamic mode - DNA-like binding energy approach @@ -15,6 +17,8 @@ enum class PatchProcessors : PatchProcessor { cooperativityBonus = 2.0, entropyPenalty = 1.0 ) + override fun extractCodeBlocks(response: String) = matcher.extractCodeBlocks(response) + override fun getInitiatorPattern() = matcher.getInitiatorPattern() }, // Strict mode - exact matching only, no fuzzy logic @@ -25,6 +29,8 @@ enum class PatchProcessors : PatchProcessor { enableSnippetPatching = false, contextSize = 5 ) + override fun extractCodeBlocks(response: String) = matcher.extractCodeBlocks(response) + override fun getInitiatorPattern() = matcher.getInitiatorPattern() }, // Lenient mode - maximum fuzzy matching @@ -39,11 +45,15 @@ enum class PatchProcessors : PatchProcessor { requireAnchorMatch = false, contextSize = 2 ) + override fun extractCodeBlocks(response: String) = matcher.extractCodeBlocks(response) + override fun getInitiatorPattern() = matcher.getInitiatorPattern() }, // Default/Fuzzy - balanced configuration Fuzzy { override val label = "Fuzzy" + override fun extractCodeBlocks(response: String) = matcher.extractCodeBlocks(response) + override fun getInitiatorPattern() = matcher.getInitiatorPattern() override val matcher = FuzzyPatchMatcher() }; diff --git a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/ThermodynamicPatchMatcher.kt b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/ThermodynamicPatchMatcher.kt index b8110f6e0..a737c251c 100644 --- a/core/src/main/kotlin/com/simiacryptus/cognotik/diff/ThermodynamicPatchMatcher.kt +++ b/core/src/main/kotlin/com/simiacryptus/cognotik/diff/ThermodynamicPatchMatcher.kt @@ -563,6 +563,13 @@ class ThermodynamicPatchMatcher( } } + override fun getInitiatorPattern(): Regex { + return FuzzyPatchMatcher.default.getInitiatorPattern() + } + override fun extractCodeBlocks(response: String): List> { + return FuzzyPatchMatcher.default.extractCodeBlocks(response) + } + /** * Normalizes a line for comparison. */ diff --git a/desktop/src/main/resources/welcome/modules/task-config.js b/desktop/src/main/resources/welcome/modules/task-config.js index d6a41d3d7..c5eaa8252 100644 --- a/desktop/src/main/resources/welcome/modules/task-config.js +++ b/desktop/src/main/resources/welcome/modules/task-config.js @@ -142,6 +142,14 @@ class TaskConfigManager { default: 'HttpClient', tooltip: 'Method used to fetch content from URLs' }, + { + id: 'processing_strategy', + label: 'Processing Method', + type: 'select', + options: ['DefaultSummarizer', 'FactChecking', 'JobMatching', 'SchemaExtraction', 'DataTableAccumulation'], + default: 'DefaultSummarizer', + tooltip: 'Strategy for processing fetched content' + }, { id: 'max_pages_per_task', label: 'Max Pages Per Task', @@ -209,7 +217,7 @@ class TaskConfigManager { { id: 'allowed_domains', label: 'Allowed Domains', - type: 'textarea', + type: 'text', placeholder: 'Enter one domain per line (e.g., example.com)', tooltip: 'List of domains that the crawler is allowed to visit' } diff --git a/gradle.properties b/gradle.properties index b82136041..1d7430fa4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ pluginName=Cognotik pluginRepositoryUrl=https://github.com/SimiaCryptus/Cognotik libraryGroup=com.simiacryptus -libraryVersion=2.0.20 +libraryVersion=2.0.21 gradleVersion=8.13 org.gradle.caching=true diff --git a/intellij/src/main/kotlin/cognotik/actions/task/FileModificationTaskAction.kt b/intellij/src/main/kotlin/cognotik/actions/task/FileModificationTaskAction.kt new file mode 100644 index 000000000..54af902ac --- /dev/null +++ b/intellij/src/main/kotlin/cognotik/actions/task/FileModificationTaskAction.kt @@ -0,0 +1,140 @@ +package cognotik.actions.task + +import cognotik.actions.BaseAction +import cognotik.actions.agent.toFile +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.simiacryptus.cognotik.CognotikAppServer +import com.simiacryptus.cognotik.apps.general.SingleTaskApp +import com.simiacryptus.cognotik.config.instance +import com.simiacryptus.cognotik.plan.OrchestrationConfig +import com.simiacryptus.cognotik.plan.tools.file.FileModificationTask +import com.simiacryptus.cognotik.plan.tools.file.FileModificationTask.Companion.FileModification +import com.simiacryptus.cognotik.platform.Session +import com.simiacryptus.cognotik.platform.file.DataStorage +import com.simiacryptus.cognotik.platform.file.UserSettingsManager +import com.simiacryptus.cognotik.platform.model.ApiChatModel +import com.simiacryptus.cognotik.util.* +import com.simiacryptus.cognotik.util.BrowseUtil.browse +import com.simiacryptus.cognotik.webui.application.AppInfoData +import com.simiacryptus.cognotik.webui.application.ApplicationServer +import java.io.File +import java.text.SimpleDateFormat + +class FileModificationTaskAction : BaseAction() { + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun handle(e: AnActionEvent) { + val root = getProjectRoot(e) ?: return + val files = getFiles(e) + + val dialog = FileModificationTaskDialog( + e.project, + root, + files + ) + + if (dialog.showAndGet()) { + try { + val taskConfig = dialog.getTaskConfig() + val orchestrationConfig = dialog.getOrchestrationConfig() + + UITools.runAsync(e.project, "Initializing File Modification Task", true) { progress -> + initializeTask(e, progress, orchestrationConfig, taskConfig, root) + } + } catch (ex: Exception) { + log.error("Failed to initialize file modification task", ex) + UITools.showError(e.project, "Failed to initialize task: ${ex.message}") + } + } + } + + private fun initializeTask( + e: AnActionEvent, + progress: ProgressIndicator, + orchestrationConfig: OrchestrationConfig, + taskConfig: FileModificationTask.FileModificationTaskExecutionConfigData, + root: File + ) { + progress.text = "Setting up session..." + val session = Session.newGlobalID() + + DataStorage.sessionPaths[session] = root + + progress.text = "Starting server..." + setupTaskSession(session, orchestrationConfig, taskConfig, root) + + Thread { + Thread.sleep(500) + try { + val uri = CognotikAppServer.getServer().server.uri.resolve("/#$session") + log.info("Opening browser to $uri") + browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + private fun setupTaskSession( + session: Session, + orchestrationConfig: OrchestrationConfig, + taskConfig: FileModificationTask.FileModificationTaskExecutionConfigData, + root: File + ) { + val app = object : SingleTaskApp( + applicationName = "File Modification Task", + path = "/fileModificationTask", + showMenubar = false, + taskType = FileModification, + taskConfig = taskConfig, + instanceFn = { model -> model.instance() ?: throw IllegalStateException("Model or Provider not set") } + ) { + override fun instance(model: ApiChatModel) = model.instance() + ?: throw IllegalStateException("Model or Provider not set") + } + + app.getSettingsFile(session, UserSettingsManager.defaultUser).writeText(orchestrationConfig.toJson()) + SessionProxyServer.chats[session] = app + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "File Modification Task", + inputCnt = 0, + stickyInput = false, + showMenubar = false + ) + SessionProxyServer.metadataStorage.setSessionName( + null, + session, + "File Modification @ ${SimpleDateFormat("HH:mm:ss").format(System.currentTimeMillis())}" + ) + } + + private fun getProjectRoot(e: AnActionEvent): File? { + val folder = e.getSelectedFolder() + return folder?.toFile ?: e.getSelectedFile()?.parent?.toFile?.let { file -> + getModuleRootForFile(file) + } + } +} + + +fun getFiles(e: AnActionEvent): List { + val selectedFiles = e.getSelectedFiles() + val relatedFiles = if (selectedFiles.isEmpty()) { + e.getSelectedFolder()?.toFile?.let { + FileSelectionUtils.filteredWalk(it) { file -> + when { + FileSelectionUtils.isLLMIgnored(file.toPath()) -> false + it.isDirectory -> true + else -> false + } + } + } ?: emptyList() + } else { + selectedFiles.map { it.toFile } + } + return relatedFiles +} + diff --git a/intellij/src/main/kotlin/cognotik/actions/task/FileModificationTaskDialog.kt b/intellij/src/main/kotlin/cognotik/actions/task/FileModificationTaskDialog.kt new file mode 100644 index 000000000..a36e02806 --- /dev/null +++ b/intellij/src/main/kotlin/cognotik/actions/task/FileModificationTaskDialog.kt @@ -0,0 +1,169 @@ +package cognotik.actions.task + +import cognotik.actions.plan.PlanConfigDialog +import cognotik.actions.plan.toApiChatModel +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.simiacryptus.cognotik.config.AppSettingsState +import com.simiacryptus.cognotik.plan.AbstractTask.TaskState +import com.simiacryptus.cognotik.plan.OrchestrationConfig +import com.simiacryptus.cognotik.plan.TaskTypeConfig +import com.simiacryptus.cognotik.plan.tools.file.FileModificationTask +import com.simiacryptus.cognotik.platform.ApplicationServices +import java.awt.Dimension +import java.io.File +import javax.swing.JComponent +import javax.swing.JSlider + +class FileModificationTaskDialog( + project: Project?, private val root: File, val files: List +) : DialogWrapper(project) { + + private val taskDescriptionArea = JBTextArea(5, 40).apply { + lineWrap = true + wrapStyleWord = true + toolTipText = "Describe what modifications should be made to the files" + } + + private val filesField = JBTextField().apply { + toolTipText = "Comma-separated list of file paths (relative to project root) to modify or create" + text = files.joinToString(", ") { it.relativeTo(root).path } + } + + private val relatedFilesField = JBTextField().apply { + toolTipText = "Comma-separated list of related files to consider for context" + } + + private val extractContentCheckbox = JBCheckBox("Extract content from non-text files", false).apply { + toolTipText = "Extract text content from PDF, HTML, and other document formats" + } + + private val includeGitDiffCheckbox = JBCheckBox("Include git diff with HEAD", false).apply { + toolTipText = "Include git diff information to show recent changes" + } + + private val visibleModelsCache by lazy { getVisibleModels() } + + private val modelCombo = ComboBox( + visibleModelsCache.distinctBy { it.modelName }.map { it.modelName }.toTypedArray() + ).apply { + maximumSize = Dimension(200, 30) + selectedItem = AppSettingsState.instance.smartModel?.model?.modelName + toolTipText = "AI model to use for this task" + } + + private val temperatureSlider = JSlider(0, 100, 70).apply { + addChangeListener { + temperatureLabel.text = "%.2f".format(value / 100.0) + } + } + + private val temperatureLabel = javax.swing.JLabel("0.70") + + private val autoFixCheckbox = JBCheckBox("Auto-apply fixes", false).apply { + toolTipText = "Automatically apply suggested changes without manual confirmation" + } + + init { + init() + title = "Configure File Modification Task" + } + + override fun createCenterPanel(): JComponent = panel { + group("Task Configuration") { + row("Task Description:") { + scrollCell(taskDescriptionArea).align(Align.FILL).comment("Describe the modifications to be made").resizableColumn() + }.resizableRow() + + row("Files to Modify:") { + cell(filesField).align(Align.FILL).comment("Comma-separated file paths (e.g., src/main.kt, src/utils.kt)") + } + + row("Related Files:") { + cell(relatedFilesField).align(Align.FILL).comment("Additional files for context (optional)") + } + + row { + cell(extractContentCheckbox) + } + + row { + cell(includeGitDiffCheckbox) + } + } + + group("Model Settings") { + row("Model:") { + cell(modelCombo).align(Align.FILL).comment("AI model to use for generating modifications") + } + + row("Temperature:") { + cell(temperatureSlider).align(Align.FILL).comment("Higher values = more creative, lower = more focused") + cell(temperatureLabel) + } + + row { + cell(autoFixCheckbox) + } + } + } + + override fun doValidate(): com.intellij.openapi.ui.ValidationInfo? { + if (taskDescriptionArea.text.isBlank()) { + return com.intellij.openapi.ui.ValidationInfo("Task description is required", taskDescriptionArea) + } + + if (filesField.text.isBlank()) { + return com.intellij.openapi.ui.ValidationInfo("At least one file must be specified", filesField) + } + + return null + } + + fun getTaskConfig(): FileModificationTask.FileModificationTaskExecutionConfigData { + val files = filesField.text.split(",").map { it.trim() }.filter { it.isNotEmpty() } + val relatedFiles = relatedFilesField.text.split(",").map { it.trim() }.filter { it.isNotEmpty() }.takeIf { it.isNotEmpty() } + + return FileModificationTask.FileModificationTaskExecutionConfigData( + task_description = taskDescriptionArea.text, + files = files, + related_files = relatedFiles, + extractContent = extractContentCheckbox.isSelected, + includeGitDiff = includeGitDiffCheckbox.isSelected, + state = TaskState.Pending + ) + } + + fun getOrchestrationConfig(): OrchestrationConfig { + val selectedModel = modelCombo.selectedItem as? String + val model = selectedModel?.let { modelName -> + visibleModelsCache.find { it.modelName == modelName }?.toApiChatModel() + } + + return OrchestrationConfig( + defaultModel = model ?: AppSettingsState.instance.smartModel ?: throw IllegalStateException("No model configured"), + parsingModel = AppSettingsState.instance.fastModel ?: throw IllegalStateException("Fast model not configured"), + temperature = temperatureSlider.value / 100.0, + autoFix = autoFixCheckbox.isSelected, + workingDir = root.absolutePath, + shellCmd = listOf( + if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash" + ), + taskSettings = mutableMapOf( + FileModificationTask.FileModification.name to TaskTypeConfig(task_type = FileModificationTask.FileModification.name) + ) + ) + } + + private fun getVisibleModels() = ApplicationServices.fileApplicationServices().userSettingsManager.getUserSettings().apis.flatMap { apiData -> + apiData.provider?.getChatModels(apiData.key!!, apiData.baseUrl)?.filter { model -> + model.provider == apiData.provider && model.modelName?.isNotBlank() == true && PlanConfigDialog.isVisible(model) + } ?: listOf() + }.distinctBy { it.modelName }.sortedBy { "${it.provider?.name} - ${it.modelName}" } +} \ No newline at end of file diff --git a/intellij/src/main/kotlin/cognotik/actions/task/GeneratePresentationAction.kt b/intellij/src/main/kotlin/cognotik/actions/task/GeneratePresentationAction.kt new file mode 100644 index 000000000..7888151ed --- /dev/null +++ b/intellij/src/main/kotlin/cognotik/actions/task/GeneratePresentationAction.kt @@ -0,0 +1,112 @@ +package cognotik.actions.task + +import cognotik.actions.BaseAction +import cognotik.actions.agent.toFile +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.simiacryptus.cognotik.CognotikAppServer +import com.simiacryptus.cognotik.apps.general.SingleTaskApp +import com.simiacryptus.cognotik.config.instance +import com.simiacryptus.cognotik.plan.OrchestrationConfig +import com.simiacryptus.cognotik.plan.tools.file.GeneratePresentationTask +import com.simiacryptus.cognotik.platform.Session +import com.simiacryptus.cognotik.platform.file.DataStorage +import com.simiacryptus.cognotik.platform.file.UserSettingsManager +import com.simiacryptus.cognotik.platform.model.ApiChatModel +import com.simiacryptus.cognotik.util.* +import com.simiacryptus.cognotik.util.BrowseUtil.browse +import com.simiacryptus.cognotik.webui.application.AppInfoData +import com.simiacryptus.cognotik.webui.application.ApplicationServer +import java.io.File +import java.text.SimpleDateFormat + +class GeneratePresentationAction : BaseAction() { + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + if (event.getSelectedFiles().isEmpty() && event.getSelectedFolder() == null) return false + return true + } + + override fun handle(e: AnActionEvent) { + val root = getProjectRoot(e) ?: return + val relatedFiles = getFiles(e) + val dialog = GeneratePresentationTaskDialog( + e.project, root, relatedFiles + ) + + if (dialog.showAndGet()) { + try { + val taskConfig = dialog.getTaskConfig() + val orchestrationConfig = dialog.getOrchestrationConfig() + + UITools.runAsync(e.project, "Initializing Presentation Generation Task", true) { progress -> + initializeTask(e, progress, orchestrationConfig, taskConfig, root) + } + } catch (ex: Exception) { + log.error("Failed to initialize presentation generation task", ex) + UITools.showError(e.project, "Failed to initialize task: ${ex.message}") + } + } + } + + private fun initializeTask( + e: AnActionEvent, + progress: ProgressIndicator, + orchestrationConfig: OrchestrationConfig, + taskConfig: GeneratePresentationTask.GeneratePresentationTaskExecutionConfigData, + root: File + ) { + progress.text = "Setting up session..." + val session = Session.newGlobalID() + + DataStorage.sessionPaths[session] = root + + progress.text = "Starting server..." + setupTaskSession(session, orchestrationConfig, taskConfig, root) + + Thread { + Thread.sleep(500) + try { + val uri = CognotikAppServer.getServer().server.uri.resolve("/#$session") + log.info("Opening browser to $uri") + browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + private fun setupTaskSession( + session: Session, orchestrationConfig: OrchestrationConfig, taskConfig: GeneratePresentationTask.GeneratePresentationTaskExecutionConfigData, root: File + ) { + val app = object : SingleTaskApp( + applicationName = "Presentation Generation Task", + path = "/generatePresentationTask", + showMenubar = false, + taskType = GeneratePresentationTask.GeneratePresentation, + taskConfig = taskConfig, + instanceFn = { model -> model.instance() ?: throw IllegalStateException("Model or Provider not set") } + ) { + override fun instance(model: ApiChatModel) = model.instance() ?: throw IllegalStateException("Model or Provider not set") + } + + app.getSettingsFile(session, UserSettingsManager.defaultUser).writeText(orchestrationConfig.toJson()) + SessionProxyServer.chats[session] = app + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Presentation Generation Task", inputCnt = 0, stickyInput = false, showMenubar = false + ) + SessionProxyServer.metadataStorage.setSessionName( + null, session, "Presentation Generation @ ${SimpleDateFormat("HH:mm:ss").format(System.currentTimeMillis())}" + ) + } + + private fun getProjectRoot(e: AnActionEvent): File? { + val folder = e.getSelectedFolder() + return folder?.toFile ?: e.getSelectedFile()?.parent?.toFile?.let { file -> + getModuleRootForFile(file) + } + } +} \ No newline at end of file diff --git a/intellij/src/main/kotlin/cognotik/actions/task/GeneratePresentationTaskDialog.kt b/intellij/src/main/kotlin/cognotik/actions/task/GeneratePresentationTaskDialog.kt new file mode 100644 index 000000000..abf83be70 --- /dev/null +++ b/intellij/src/main/kotlin/cognotik/actions/task/GeneratePresentationTaskDialog.kt @@ -0,0 +1,220 @@ +package cognotik.actions.task + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.simiacryptus.cognotik.config.AppSettingsState +import com.simiacryptus.cognotik.plan.OrchestrationConfig +import com.simiacryptus.cognotik.plan.tools.file.GeneratePresentationTask +import com.simiacryptus.cognotik.platform.ApplicationServices +import cognotik.actions.plan.PlanConfigDialog +import cognotik.actions.plan.toApiChatModel +import com.simiacryptus.cognotik.plan.AbstractTask.TaskState +import java.awt.Dimension +import java.io.File +import javax.swing.JComponent +import javax.swing.JSlider +import javax.swing.JSpinner +import javax.swing.SpinnerNumberModel + +class GeneratePresentationTaskDialog( + project: Project?, + private val root: File, + val relatedFiles: List +) : DialogWrapper(project) { + + private val taskDescriptionArea = JBTextArea(8, 40).apply { + lineWrap = true + wrapStyleWord = true + toolTipText = "Describe the presentation including topic, key points, target audience, and desired style" + } + + private val htmlFileField = JBTextField().apply { + toolTipText = "Path for the HTML presentation file to create (must end with .html)" + text = "${relatedFiles.firstOrNull()?.nameWithoutExtension?.let { "${it}_presentation" } ?: "presentation"}.html" + } + + private val relatedFilesField = JBTextField().apply { + toolTipText = "Comma-separated list of related files to consider for context (e.g., reference materials)" + text = relatedFiles.joinToString(", ") { it.relativeTo(root).path } + } + + private val generateImagesCheckbox = JBCheckBox("Generate images for key slides", false).apply { + toolTipText = "Use AI to generate images for important slides in the presentation" + addActionListener { + imageCountSpinner.isEnabled = isSelected + imageModelCombo.isEnabled = isSelected + } + } + + private val imageCountSpinner = JSpinner(SpinnerNumberModel(5, 1, 10, 1)).apply { + toolTipText = "Maximum number of images to generate (1-10)" + isEnabled = false + } + + private val visibleModelsCache by lazy { getVisibleModels() } + + private val modelCombo = ComboBox( + visibleModelsCache.distinctBy { it.modelName }.map { it.modelName }.toTypedArray() + ).apply { + maximumSize = Dimension(200, 30) + selectedItem = AppSettingsState.instance.smartModel?.model?.modelName + toolTipText = "AI model to use for generating presentation content" + } + + private val imageModelCombo = ComboBox( + visibleModelsCache + .distinctBy { it.modelName } + .map { it.modelName } + .toTypedArray() + ).apply { + maximumSize = Dimension(200, 30) + selectedItem = AppSettingsState.instance.imageChatModel?.model?.modelName + toolTipText = "AI model to use for generating images" + isEnabled = false + } + + private val temperatureSlider = JSlider(0, 100, 70).apply { + addChangeListener { + temperatureLabel.text = "%.2f".format(value / 100.0) + } + } + + private val temperatureLabel = javax.swing.JLabel("0.70") + + private val autoFixCheckbox = JBCheckBox("Auto-apply generated presentation", false).apply { + toolTipText = "Automatically write the generated presentation files without manual confirmation" + } + + init { + init() + title = "Configure Presentation Generation Task" + } + + override fun createCenterPanel(): JComponent = panel { + group("Presentation Configuration") { + row("HTML File:") { + cell(htmlFileField) + .align(Align.FILL) + .comment("Output path for the presentation file (e.g., presentation.html, slides/demo.html)") + } + + row("Presentation Description:") { + scrollCell(taskDescriptionArea) + .align(Align.FILL) + .comment("Describe the presentation topic, key points, target audience, number of slides, and style preferences") + .resizableColumn() + }.resizableRow() + + row("Related Files:") { + cell(relatedFilesField) + .align(Align.FILL) + .comment("Additional files for context (optional)") + } + } + + group("Image Generation") { + row { + cell(generateImagesCheckbox) + } + + row("Maximum Images:") { + cell(imageCountSpinner) + .comment("Maximum number of images to generate for key slides (1-10)") + } + + row("Image Model:") { + cell(imageModelCombo) + .align(Align.FILL) + .comment("AI model for image generation") + } + } + + group("Model Settings") { + row("Text Model:") { + cell(modelCombo) + .align(Align.FILL) + .comment("AI model for generating presentation content") + } + + row("Temperature:") { + cell(temperatureSlider) + .align(Align.FILL) + .comment("Higher values = more creative, lower = more focused") + cell(temperatureLabel) + } + + row { + cell(autoFixCheckbox) + } + } + } + + override fun doValidate(): com.intellij.openapi.ui.ValidationInfo? { + if (htmlFileField.text.isBlank()) { + return com.intellij.openapi.ui.ValidationInfo("HTML file path is required", htmlFileField) + } + + if (!htmlFileField.text.endsWith(".html", ignoreCase = true)) { + return com.intellij.openapi.ui.ValidationInfo("File must have .html extension", htmlFileField) + } + + return null + } + + fun getTaskConfig(): GeneratePresentationTask.GeneratePresentationTaskExecutionConfigData { + val relatedFiles = relatedFilesField.text.split(",").map { it.trim() }.filter { it.isNotEmpty() } + .takeIf { it.isNotEmpty() } + + return GeneratePresentationTask.GeneratePresentationTaskExecutionConfigData( + files = listOf(htmlFileField.text), + related_files = relatedFiles, + task_description = taskDescriptionArea.text, + generate_images = generateImagesCheckbox.isSelected, + image_model = imageModelCombo.selectedItem as? String ?: "DallE3", + max_images = imageCountSpinner.value as Int, + state = TaskState.Pending + ) + } + + fun getOrchestrationConfig(): OrchestrationConfig { + val selectedModel = modelCombo.selectedItem as? String + val model = selectedModel?.let { modelName -> + visibleModelsCache.find { it.modelName == modelName }?.toApiChatModel() + } + + val selectedImageModel = imageModelCombo.selectedItem as? String + val imageModel = selectedImageModel?.let { modelName -> + visibleModelsCache.find { it.modelName == modelName }?.toApiChatModel() + } + + return OrchestrationConfig( + defaultModel = model ?: AppSettingsState.instance.smartModel + ?: throw IllegalStateException("No model configured"), + parsingModel = AppSettingsState.instance.fastModel + ?: throw IllegalStateException("Fast model not configured"), + imageChatModel = imageModel ?: AppSettingsState.instance.imageChatModel + ?: throw IllegalStateException("No image model configured"), + temperature = temperatureSlider.value / 100.0, + autoFix = autoFixCheckbox.isSelected, + workingDir = root.absolutePath, + shellCmd = listOf( + if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash" + ) + ) + } + + private fun getVisibleModels() = + ApplicationServices.fileApplicationServices().userSettingsManager.getUserSettings().apis.flatMap { apiData -> + apiData.provider?.getChatModels(apiData.key!!, apiData.baseUrl)?.filter { model -> + model.provider == apiData.provider && + model.modelName?.isNotBlank() == true && + PlanConfigDialog.isVisible(model) + } ?: listOf() + }.distinctBy { it.modelName }.sortedBy { "${it.provider?.name} - ${it.modelName}" } +} \ No newline at end of file diff --git a/intellij/src/main/kotlin/cognotik/actions/task/WriteHtmlAction.kt b/intellij/src/main/kotlin/cognotik/actions/task/WriteHtmlAction.kt new file mode 100644 index 000000000..8e0690ee8 --- /dev/null +++ b/intellij/src/main/kotlin/cognotik/actions/task/WriteHtmlAction.kt @@ -0,0 +1,112 @@ +package cognotik.actions.task + +import cognotik.actions.BaseAction +import cognotik.actions.agent.toFile +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.simiacryptus.cognotik.CognotikAppServer +import com.simiacryptus.cognotik.apps.general.SingleTaskApp +import com.simiacryptus.cognotik.config.instance +import com.simiacryptus.cognotik.plan.OrchestrationConfig +import com.simiacryptus.cognotik.plan.tools.file.WriteHtmlTask +import com.simiacryptus.cognotik.platform.Session +import com.simiacryptus.cognotik.platform.file.DataStorage +import com.simiacryptus.cognotik.platform.file.UserSettingsManager +import com.simiacryptus.cognotik.platform.model.ApiChatModel +import com.simiacryptus.cognotik.util.* +import com.simiacryptus.cognotik.util.BrowseUtil.browse +import com.simiacryptus.cognotik.webui.application.AppInfoData +import com.simiacryptus.cognotik.webui.application.ApplicationServer +import java.io.File +import java.text.SimpleDateFormat + +class WriteHtmlAction : BaseAction() { + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + if (event.getSelectedFiles().isEmpty() && event.getSelectedFolder() == null) return false + return true + } + + override fun handle(e: AnActionEvent) { + val root = getProjectRoot(e) ?: return + val relatedFiles = getFiles(e) + val dialog = WriteHtmlTaskDialog( + e.project, root, relatedFiles + ) + + if (dialog.showAndGet()) { + try { + val taskConfig = dialog.getTaskConfig() + val orchestrationConfig = dialog.getOrchestrationConfig() + + UITools.runAsync(e.project, "Initializing HTML Generation Task", true) { progress -> + initializeTask(e, progress, orchestrationConfig, taskConfig, root) + } + } catch (ex: Exception) { + log.error("Failed to initialize HTML generation task", ex) + UITools.showError(e.project, "Failed to initialize task: ${ex.message}") + } + } + } + + private fun initializeTask( + e: AnActionEvent, + progress: ProgressIndicator, + orchestrationConfig: OrchestrationConfig, + taskConfig: WriteHtmlTask.WriteHtmlTaskExecutionConfigData, + root: File + ) { + progress.text = "Setting up session..." + val session = Session.newGlobalID() + + DataStorage.sessionPaths[session] = root + + progress.text = "Starting server..." + setupTaskSession(session, orchestrationConfig, taskConfig, root) + + Thread { + Thread.sleep(500) + try { + val uri = CognotikAppServer.getServer().server.uri.resolve("/#$session") + log.info("Opening browser to $uri") + browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + private fun setupTaskSession( + session: Session, orchestrationConfig: OrchestrationConfig, taskConfig: WriteHtmlTask.WriteHtmlTaskExecutionConfigData, root: File + ) { + val app = object : SingleTaskApp( + applicationName = "HTML Generation Task", + path = "/writeHtmlTask", + showMenubar = false, + taskType = WriteHtmlTask.WriteHtml, + taskConfig = taskConfig, + instanceFn = { model -> model.instance() ?: throw IllegalStateException("Model or Provider not set") } + ) { + override fun instance(model: ApiChatModel) = model.instance() ?: throw IllegalStateException("Model or Provider not set") + } + + app.getSettingsFile(session, UserSettingsManager.defaultUser).writeText(orchestrationConfig.toJson()) + SessionProxyServer.chats[session] = app + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "HTML Generation Task", inputCnt = 0, stickyInput = false, showMenubar = false + ) + SessionProxyServer.metadataStorage.setSessionName( + null, session, "HTML Generation @ ${SimpleDateFormat("HH:mm:ss").format(System.currentTimeMillis())}" + ) + } + + private fun getProjectRoot(e: AnActionEvent): File? { + val folder = e.getSelectedFolder() + return folder?.toFile ?: e.getSelectedFile()?.parent?.toFile?.let { file -> + getModuleRootForFile(file) + } + } +} diff --git a/intellij/src/main/kotlin/cognotik/actions/task/WriteHtmlTaskDialog.kt b/intellij/src/main/kotlin/cognotik/actions/task/WriteHtmlTaskDialog.kt new file mode 100644 index 000000000..081e536cc --- /dev/null +++ b/intellij/src/main/kotlin/cognotik/actions/task/WriteHtmlTaskDialog.kt @@ -0,0 +1,221 @@ +package cognotik.actions.task + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.simiacryptus.cognotik.config.AppSettingsState +import com.simiacryptus.cognotik.plan.OrchestrationConfig +import com.simiacryptus.cognotik.plan.tools.file.WriteHtmlTask +import com.simiacryptus.cognotik.platform.ApplicationServices +import cognotik.actions.plan.PlanConfigDialog +import cognotik.actions.plan.toApiChatModel +import com.simiacryptus.cognotik.plan.AbstractTask.TaskState +import java.awt.Dimension +import java.io.File +import javax.swing.JComponent +import javax.swing.JSlider +import javax.swing.JSpinner +import javax.swing.SpinnerNumberModel + +class WriteHtmlTaskDialog( + project: Project?, + private val root: File, + val relatedFiles: List +) : DialogWrapper(project) { + + private val taskDescriptionArea = JBTextArea(8, 40).apply { + lineWrap = true + wrapStyleWord = true + toolTipText = "Describe the HTML page to create, including layout, styling, and functionality requirements" + } + + private val htmlFileField = JBTextField().apply { + toolTipText = "Path for the HTML file to create (must end with .html)" + text = "${relatedFiles.firstOrNull()?.nameWithoutExtension ?: "index"}.html" + } + + private val relatedFilesField = JBTextField().apply { + toolTipText = "Comma-separated list of related files to consider for context (e.g., existing templates)" + text = relatedFiles.joinToString(", ") { it.relativeTo(root).path } + } + + private val generateImagesCheckbox = JBCheckBox("Generate images for the page", false).apply { + toolTipText = "Use AI to generate images for the HTML page" + addActionListener { + imageCountSpinner.isEnabled = isSelected + } + } + + private val imageCountSpinner = JSpinner(SpinnerNumberModel(3, 0, 10, 1)).apply { + toolTipText = "Number of images to generate (0-10)" + isEnabled = false + } + + private val visibleModelsCache by lazy { getVisibleModels() } + + private val modelCombo = ComboBox( + visibleModelsCache.distinctBy { it.modelName }.map { it.modelName }.toTypedArray() + ).apply { + maximumSize = Dimension(200, 30) + selectedItem = AppSettingsState.instance.smartModel?.model?.modelName + toolTipText = "AI model to use for generating HTML, CSS, and JavaScript" + } + + private val imageModelCombo = ComboBox( + visibleModelsCache + .distinctBy { it.modelName } + .map { it.modelName } + .toTypedArray() + ).apply { + maximumSize = Dimension(200, 30) + selectedItem = AppSettingsState.instance.imageChatModel?.model?.modelName + toolTipText = "AI model to use for generating images" + } + + private val temperatureSlider = JSlider(0, 100, 70).apply { + addChangeListener { + temperatureLabel.text = "%.2f".format(value / 100.0) + } + } + + private val temperatureLabel = javax.swing.JLabel("0.70") + + private val autoFixCheckbox = JBCheckBox("Auto-apply generated HTML", false).apply { + toolTipText = "Automatically write the generated HTML file without manual confirmation" + } + + init { + init() + title = "Configure HTML Generation Task" + } + + override fun createCenterPanel(): JComponent = panel { + group("HTML Configuration") { + row("HTML File:") { + cell(htmlFileField) + .align(Align.FILL) + .comment("Output path for the HTML file (e.g., index.html, pages/about.html)") + } + + row("Page Description:") { + scrollCell(taskDescriptionArea) + .align(Align.FILL) + .comment("Describe the page layout, styling, functionality, and any specific requirements") + .resizableColumn() + }.resizableRow() + + row("Related Files:") { + cell(relatedFilesField) + .align(Align.FILL) + .comment("Additional files for context (optional)") + } + } + + group("Image Generation") { + row { + cell(generateImagesCheckbox) + } + + row("Number of Images:") { + cell(imageCountSpinner) + .comment("How many images to generate (0-10)") + } + + row("Image Model:") { + cell(imageModelCombo) + .align(Align.FILL) + .comment("AI model for image generation") + } + } + + group("Model Settings") { + row("Text Model:") { + cell(modelCombo) + .align(Align.FILL) + .comment("AI model for generating HTML, CSS, and JavaScript") + } + + row("Temperature:") { + cell(temperatureSlider) + .align(Align.FILL) + .comment("Higher values = more creative, lower = more focused") + cell(temperatureLabel) + } + + row { + cell(autoFixCheckbox) + } + } + } + + override fun doValidate(): com.intellij.openapi.ui.ValidationInfo? { +// if (taskDescriptionArea.text.isBlank()) { +// return com.intellij.openapi.ui.ValidationInfo("Page description is required", taskDescriptionArea) +// } + + if (htmlFileField.text.isBlank()) { + return com.intellij.openapi.ui.ValidationInfo("HTML file path is required", htmlFileField) + } + + if (!htmlFileField.text.endsWith(".html", ignoreCase = true)) { + return com.intellij.openapi.ui.ValidationInfo("File must have .html extension", htmlFileField) + } + + return null + } + + fun getTaskConfig(): WriteHtmlTask.WriteHtmlTaskExecutionConfigData { + val relatedFiles = relatedFilesField.text.split(",").map { it.trim() }.filter { it.isNotEmpty() } + .takeIf { it.isNotEmpty() } + + return WriteHtmlTask.WriteHtmlTaskExecutionConfigData( + files = listOf(htmlFileField.text), + related_files = relatedFiles, + task_description = taskDescriptionArea.text, + generate_images = generateImagesCheckbox.isSelected, + image_count = imageCountSpinner.value as Int, + state = TaskState.Pending + ) + } + + fun getOrchestrationConfig(): OrchestrationConfig { + val selectedModel = modelCombo.selectedItem as? String + val model = selectedModel?.let { modelName -> + visibleModelsCache.find { it.modelName == modelName }?.toApiChatModel() + } + + val selectedImageModel = imageModelCombo.selectedItem as? String + val imageModel = selectedImageModel?.let { modelName -> + visibleModelsCache.find { it.modelName == modelName }?.toApiChatModel() + } + + return OrchestrationConfig( + defaultModel = model ?: AppSettingsState.instance.smartModel + ?: throw IllegalStateException("No model configured"), + parsingModel = AppSettingsState.instance.fastModel + ?: throw IllegalStateException("Fast model not configured"), + imageChatModel = imageModel ?: AppSettingsState.instance.smartModel + ?: throw IllegalStateException("No image model configured"), + temperature = temperatureSlider.value / 100.0, + autoFix = autoFixCheckbox.isSelected, + workingDir = root.absolutePath, + shellCmd = listOf( + if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash" + ) + ) + } + + private fun getVisibleModels() = + ApplicationServices.fileApplicationServices().userSettingsManager.getUserSettings().apis.flatMap { apiData -> + apiData.provider?.getChatModels(apiData.key!!, apiData.baseUrl)?.filter { model -> + model.provider == apiData.provider && + model.modelName?.isNotBlank() == true && + PlanConfigDialog.isVisible(model) + } ?: listOf() + }.distinctBy { it.modelName }.sortedBy { "${it.provider?.name} - ${it.modelName}" } +} \ No newline at end of file diff --git a/intellij/src/main/resources/META-INF/plugin.xml b/intellij/src/main/resources/META-INF/plugin.xml index 95bd4d87b..c61ba7540 100644 --- a/intellij/src/main/resources/META-INF/plugin.xml +++ b/intellij/src/main/resources/META-INF/plugin.xml @@ -91,7 +91,7 @@ description="Analyze and generate patches for multiple files simultaneously, considering the broader project context"> - + + + + + + + + + , + private val taskConfig: TaskExecutionConfig, + val instanceFn: ((ApiChatModel) -> ChatInterface)? +) : ApplicationServer( + applicationName = applicationName, + path = path, + showMenubar = showMenubar, + root = dataStorageRoot, +) { + private val log = LoggerFactory.getLogger(SingleTaskApp::class.java) + + override val stickyInput = false + override val inputCnt: Int = 0 + + @Suppress("UNCHECKED_CAST") + override fun initSettings(session: Session): T = OrchestrationConfig() as T + + abstract fun instance(model: ApiChatModel): ChatInterface + + override fun newSession( + user: User?, session: Session + ): SocketManager { + val socketManager = super.newSession(user, session) + val settings = getSettings(session, user, OrchestrationConfig::class.java) + OrchestrationConfig.instanceFn = instanceFn + socketManager.newTask(cancelable = false, root = true).expandable( + "Session Info", """ +Session ID: `${session}` + +Start Time: `${SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())}` + +Root: `${settings?.absoluteWorkingDir}` + +Session Location: `${dataStorage.getSessionDir(user, session).absolutePath}` + +Data Location: `${dataStorage.getDataDir(user, session).absolutePath}` + +Task Type: `${taskType.name}` + + """.renderMarkdown() + ) + socketManager.pool.submit { executeTask(session, user, socketManager, settings) } + return socketManager + } + + private fun executeTask( + session: Session, user: User?, ui: SocketManager, settings: OrchestrationConfig? + ) { + try { + val orchestrationConfig = settings?.apply { + absoluteWorkingDir?.let { DataStorage.sessionPaths[session] = File(it) } + } ?: throw IllegalStateException("OrchestrationConfig not found in session settings") + + val task = ui.newTask(true) + + // Get the task implementation + val taskImpl = TaskType.getImpl( + orchestrationConfig = orchestrationConfig, taskType = taskType, planTask = taskConfig + ) + + // Execute the task + taskImpl.run( + agent = TaskOrchestrator( + user = user, + session = session, + dataStorage = ui.dataStorage!!, + root = orchestrationConfig.absoluteWorkingDir?.let { File(it).toPath() } ?: ui.dataStorage.getSessionDir(user, session).toPath() ?: File(".").toPath() + ), + messages = listOf(taskConfig.task_description ?: "Execute task"), + task = task, + resultFn = { result -> + task.complete(result) + }, + orchestrationConfig = orchestrationConfig + ) + + } catch (e: Throwable) { + log.error("Error executing task", e) + ui.newTask().error(e) + } + } + + override fun userMessage( + session: Session, user: User?, userMessage: String, ui: SocketManager + ) { + // Single task apps don't accept user messages after initialization + ui.newTask().error( + IllegalStateException("This is a single-task application. User messages are not supported.") + ) + } +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/apps/general/UnifiedPlanApp.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/apps/general/UnifiedPlanApp.kt index 24490680b..feacfa83e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/apps/general/UnifiedPlanApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/apps/general/UnifiedPlanApp.kt @@ -133,7 +133,7 @@ ${settings?.toJson()} ui: SocketManager ) { try { - val settings: OrchestrationConfig = try { + val settings = try { getSettings(session, user, OrchestrationConfig::class.java) } catch (e: Exception) { log.error("Error retrieving orchestration config, using default", e) diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/OrchestrationConfig.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/OrchestrationConfig.kt index cfee0a562..b00f1061c 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/OrchestrationConfig.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/OrchestrationConfig.kt @@ -67,8 +67,7 @@ class OrchestrationConfig( @JsonIgnore - fun instance(model: ApiChatModel) = instanceFn?.let { it(model) } - ?: throw IllegalStateException("Instance function not set") + fun instance(model: ApiChatModel) = instanceFn?.let { it(model) } ?: throw IllegalStateException("Instance function not set") @get:JsonIgnore val absoluteWorkingDir diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/TaskType.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/TaskType.kt index f11835ff3..bc037eadc 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/TaskType.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/TaskType.kt @@ -38,7 +38,18 @@ class TaskType( companion object { - init { + private val taskConstructors by lazy { + val taskConstructors: MutableMap, (OrchestrationConfig, TaskExecutionConfig?) -> AbstractTask> = mutableMapOf() + + fun registerConstructor( + taskType: TaskType, constructor: (OrchestrationConfig, T?) -> AbstractTask + ) { + taskConstructors[taskType] = { settings: OrchestrationConfig, task: TaskExecutionConfig? -> + constructor(settings, task as T?) as AbstractTask + } + register(taskType) + } + registerConstructor(ChainOfThought) { settings, task -> ChainOfThoughtTask(settings, task) } @@ -192,21 +203,13 @@ class TaskType( registerConstructor(GenerateImageTask.GenerateImage) { settings, task -> GenerateImageTask(settings, task) } - } - - fun registerConstructor( - taskType: TaskType, constructor: (OrchestrationConfig, T?) -> AbstractTask - ) { - taskConstructors[taskType] = { settings: OrchestrationConfig, task: TaskExecutionConfig? -> - constructor(settings, task as T?) as AbstractTask - } - register(taskType) + taskConstructors.toMap() } fun values() = values(TaskType::class.java) fun getImpl( - orchestrationConfig: OrchestrationConfig, planTask: TaskExecutionConfig?, strict: Boolean = true + orchestrationConfig: OrchestrationConfig, planTask: TaskExecutionConfig? ) = getImpl( orchestrationConfig = orchestrationConfig, taskType = planTask?.task_type?.let { valueOf(it) } ?: throw RuntimeException("Task type not specified"), @@ -222,10 +225,15 @@ class TaskType( return constructor(orchestrationConfig, planTask) } - fun getAvailableTaskTypes(orchestrationConfig: OrchestrationConfig) = - orchestrationConfig.taskSettings.mapNotNull { x -> valueOf(x.value.task_type ?: return@mapNotNull null) } + fun getAvailableTaskTypes(orchestrationConfig: OrchestrationConfig): List> { + @Suppress("SENSELESS_COMPARISON") require(taskConstructors != null) { "Task constructors not initialized" } // Trigger lazy initialization + return orchestrationConfig.taskSettings.mapNotNull { x -> valueOf(x.value.task_type ?: return@mapNotNull null) } + } - fun valueOf(name: String): TaskType<*, *> = valueOf(TaskType::class.java, name) + fun valueOf(name: String): TaskType<*, *> { + @Suppress("SENSELESS_COMPARISON") require(taskConstructors != null) { "Task constructors not initialized" } // Trigger lazy initialization + return valueOf(TaskType::class.java, name) + } private fun register(taskType: TaskType<*, *>) = register(TaskType::class.java, taskType) } @@ -235,5 +243,3 @@ class TaskType( class TaskTypeSerializer : DynamicEnumSerializer>(TaskType::class.java) class TaskTypeDeserializer : DynamicEnumDeserializer>(TaskType::class.java) - -private val taskConstructors = mutableMapOf, (OrchestrationConfig, TaskExecutionConfig?) -> AbstractTask>() diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/SelfHealingTask.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/SelfHealingTask.kt index 48cefd56b..795e4507e 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/SelfHealingTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/SelfHealingTask.kt @@ -18,6 +18,26 @@ import kotlin.io.path.exists class SelfHealingTask( orchestrationConfig: OrchestrationConfig, planTask: SelfHealingTaskExecutionConfigData? ) : AbstractTask(orchestrationConfig, planTask) { + + companion object { + private val log = LoggerFactory.getLogger(SelfHealingTask::class.java) + val SelfHealing = TaskType( + "SelfHealing", + SelfHealingTaskExecutionConfigData::class.java, + SelfHealingTaskTypeConfig::class.java, + "Run a command and automatically fix any issues that arise", + """ + Executes a command and automatically fixes any issues that arise. +
    +
  • Specify commands and working directories
  • +
  • Supports multiple commands and directories
  • +
  • Interactive approval mode
  • +
  • Output diff formatting
  • +
+ """ + ) + } + class SelfHealingTaskTypeConfig( task_type: String? = null, model: ApiChatModel? = null, @@ -176,23 +196,4 @@ class SelfHealingTask( return markdownTranscript } - companion object { - private val log = LoggerFactory.getLogger(SelfHealingTask::class.java) - val SelfHealing = TaskType( - "SelfHealing", - SelfHealingTaskExecutionConfigData::class.java, - SelfHealingTaskTypeConfig::class.java, - "Run a command and automatically fix any issues that arise", - """ - Executes a command and automatically fixes any issues that arise. -
    -
  • Specify commands and working directories
  • -
  • Supports multiple commands and directories
  • -
  • Interactive approval mode
  • -
  • Output diff formatting
  • -
- """ - ) - - } } diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/GeneratePresentationTask.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/GeneratePresentationTask.kt index 8ca73c9fa..70058b1c2 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/GeneratePresentationTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/GeneratePresentationTask.kt @@ -17,6 +17,8 @@ import org.slf4j.Logger import java.io.FileOutputStream import javax.imageio.ImageIO +private const val TT = """```""" + class GeneratePresentationTask( orchestrationConfig: OrchestrationConfig, planTask: GeneratePresentationTaskExecutionConfigData? @@ -144,6 +146,12 @@ class GeneratePresentationTask( val outlinePrompt = """ You are an expert presentation designer tasked with creating a Reveal.js presentation. +## Standard CSS Already Included: +The following standard CSS is already included and should not be duplicated: +${TT}css +$standardCss +$TT + ## Requirements: ${executionConfig?.task_description ?: "Create a presentation as specified"} @@ -169,7 +177,7 @@ class GeneratePresentationTask( ## Output Format: Provide ONLY the slide sections within a code block (no DOCTYPE, html, head, or body tags): -```html +${TT}html

Title

Subtitle

@@ -188,7 +196,7 @@ Provide ONLY the slide sections within a code block (no DOCTYPE, html, head, or Detailed speaker notes explaining the content.
-``` +$TT """.trimIndent() newTask.add(MarkdownUtil.renderMarkdown("### Step 1: Generating Presentation Structure", ui = ui)) @@ -256,42 +264,40 @@ $enhancedSlideContent """.trimIndent() - // Step 2: Generate custom CSS +// Step 2: Generate custom CSS val cssPrompt = """ - Based on the following Reveal.js presentation HTML, generate custom CSS styling. +Based on the following Reveal.js presentation HTML, generate custom CSS styling. -## Slide Content: -```html -$slideContent -``` + ## Slide Content: + ${TT}html + $slideContent +$TT ## Requirements: ${executionConfig?.task_description ?: "Create appropriate styling for the presentation"} ## Instructions: -1. Create CSS that enhances the Reveal.js black theme with custom styling -2. Style the control container (#controlsContainer) with: - - Fixed positioning at the top - - Professional appearance - - Responsive design -3. Add custom styles for: +1. Create ONLY additional custom CSS that enhances the presentation +2. DO NOT duplicate any styles already present in the standard CSS above +3. Focus on adding custom styles for: - .subtitle class for subtitle text - .fade-in-text for animated text - .intro-points for bullet point lists - Custom slide transitions and animations -4. Ensure readability with appropriate: + - Any slide-specific styling based on the content +4. Only add new styles that complement the existing CSS, such as: - Font sizes and weights - Color contrasts - Spacing and padding -5. Add hover effects for interactive elements -6. Include responsive design for mobile devices -7. Use CSS variables for easy theme customization + - Hover effects for interactive elements +5. Keep the CSS minimal and avoid conflicts with existing styles +6. Use CSS variables defined in the standard CSS where applicable ## Output Format: -Provide only the CSS code within a code block: -```css +Provide only the ADDITIONAL custom CSS code within a code block (no duplicates): +${TT}css /* Custom Presentation Styles */ -``` +$TT """.trimIndent() newTask.add(MarkdownUtil.renderMarkdown("### Step 2: Generating Custom CSS", ui = ui)) @@ -315,7 +321,7 @@ Provide only the CSS code within a code block: newTask.add(MarkdownUtil.renderMarkdown("### Generated Files Preview", ui = ui)) filesToWrite.forEach { (filename, content) -> newTask.add(MarkdownUtil.renderMarkdown("#### $filename", ui = ui)) - newTask.add(MarkdownUtil.renderMarkdown("```${getFileExtension(filename)}\n$content\n```", ui = ui)) + newTask.add(MarkdownUtil.renderMarkdown("$TT${getFileExtension(filename)}\n$content\n${TT}", ui = ui)) } try { @@ -356,7 +362,7 @@ Provide only the CSS code within a code block: private fun extractCodeFromResponse(response: String, vararg languages: String): String { // Try to extract code from code blocks with specified languages for (lang in languages) { - val codeBlockRegex = "```$lang\\s*([\\s\\S]*?)```".toRegex() + val codeBlockRegex = "$TT$lang\\s*([\\s\\S]*?)${TT}".toRegex() val match = codeBlockRegex.find(response) if (match != null) { return match.groupValues[1].trim() @@ -364,7 +370,7 @@ Provide only the CSS code within a code block: } // Try generic code block - val genericBlockRegex = "```\\s*([\\s\\S]*?)```".toRegex() + val genericBlockRegex = "$TT\\s*([\\s\\S]*?)${TT}".toRegex() val genericMatch = genericBlockRegex.find(response) if (genericMatch != null) { return genericMatch.groupValues[1].trim() diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/WriteHtmlTask.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/WriteHtmlTask.kt index 02a2e60b2..15f4e03e9 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/WriteHtmlTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/file/WriteHtmlTask.kt @@ -57,10 +57,6 @@ class WriteHtmlTask( return "WriteHtmlTaskExecutionConfigData: file must have .html extension, got: $htmlFile" } - // Validate task description is provided - if (task_description.isNullOrBlank()) { - return "WriteHtmlTaskExecutionConfigData: task_description cannot be null or blank" - } // Validate image count if (image_count < 0 || image_count > 10) { image_count = image_count.coerceIn(0, 10) diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/CrawlerAgentTask.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/CrawlerAgentTask.kt index eaa549ab1..90374cc97 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/CrawlerAgentTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/CrawlerAgentTask.kt @@ -33,6 +33,7 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.atomic.AtomicInteger import java.util.regex.Pattern import kotlin.math.min +import kotlin.random.Random class CrawlerAgentTask( orchestrationConfig: OrchestrationConfig, @@ -118,14 +119,10 @@ class CrawlerAgentTask( } var selenium: Selenium2S3? = null - val urlContentCache = ConcurrentHashMap() private val robotsTxtParser = RobotsTxtParser() private val pageQueueLock = Object() - - // Use a priority queue that sorts by calculated priority (higher first) - private val pageQueue = java.util.PriorityQueue( - compareByDescending { it.calculatePriority() }) + private val pageQueue = java.util.PriorityQueue(compareByDescending { it.calculatePriority() }) private val seenUrls = ConcurrentHashMap.newKeySet() override fun promptSegment(): String { @@ -188,8 +185,6 @@ class CrawlerAgentTask( var error: String? = null var processingTimeMs: Long = 0 - // Priority calculation: higher relevance and lower depth = higher priority - fun calculatePriority(): Double = relevance_score / (depth + 1.0) override fun validate(): String? { if (url.isNullOrBlank()) { return "link cannot be null or blank" @@ -204,6 +199,9 @@ class CrawlerAgentTask( } } + // Priority calculation: higher relevance and lower depth = higher priority + fun LinkData.calculatePriority(): Double = relevance_score // / (depth + 1.0) + enum class PageType { Error, Irrelevant, OK } @@ -215,9 +213,6 @@ class CrawlerAgentTask( val link_data: List? = null, ) : ValidatedObject { override fun validate(): String? { - if (page_type == PageType.OK && page_information == null) { - return "page_information is required when page_type is OK" - } link_data?.forEach { linkData -> linkData.validate()?.let { return it } } @@ -495,7 +490,7 @@ class CrawlerAgentTask( val totalTime = System.currentTimeMillis() - startTime log.info("CrawlerAgentTask completed: total_time=${totalTime}ms, pages_processed=${processedCount.get()}, errors=${errorCount.get()}, success_rate=${if (processedCount.get() > 0) ((processedCount.get() - errorCount.get()) * 100 / processedCount.get()) else 0}%") // Add page queue details tab - addPageQueueDetailsTab(crawlTabs, crawlTask, processedCount.get(), errorCount.get()) + addPageQueueDetailsTab(tabs, processedCount.get(), errorCount.get()) task.complete("Completed in ${totalTime / 1000} seconds, processed ${processedCount.get()} pages with ${errorCount.get()} errors.") // Write completion stats to transcript @@ -517,15 +512,25 @@ class CrawlerAgentTask( return errorMessage } - val summaryTask = task.ui.newTask(false) - tabs["Final Summary"] = summaryTask.placeholder - // Use strategy to generate final output val finalOutput = try { log.info("Generating final output using strategy: ${strategyType.name}") - processingStrategy.generateFinalOutput( - allPageResults.values.toList(), processingContext - ) + buildString { + appendLine("# Final Output") + appendLine(processingStrategy.generateFinalOutput(allPageResults.values.toList(), processingContext)) + appendLine("# Remaining Queue") + synchronized(pageQueueLock) { + if (pageQueue.isEmpty()) { + appendLine("No remaining pages in the queue.") + } else { + appendLine("The following pages were not processed:") + var index = 1 + pageQueue.toList().sortedBy { -it.calculatePriority() }.forEach { linkData -> + appendLine("${index++}. [${linkData.title ?: linkData.url}](${linkData.url}), Relevance Score: ${linkData.relevance_score}") + } + } + } + } } catch (e: Exception) { log.error("Failed to generate final output using strategy, falling back to basic summary", e) if (typeConfig.create_final_summary != false && analysisResults.length > (typeConfig.max_final_output_size ?: 15000)) { @@ -536,7 +541,6 @@ class CrawlerAgentTask( } try { - summaryTask.add(finalOutput.renderMarkdown) task.update() // Write final summary to transcript transcriptStream?.let { stream -> @@ -563,11 +567,7 @@ class CrawlerAgentTask( val (link, file) = task.createFile("crawler_transcript.md") val transcriptStream = file?.outputStream() task.complete( - "Writing transcript to $link " + "html " + "pdf" + "Writing transcript to $link " + "html " ) log.info("Initialized transcript file: $link") transcriptStream @@ -585,8 +585,14 @@ class CrawlerAgentTask( appendLine("**Started:** ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}") appendLine() appendLine("**Search Query:** ${executionConfig?.search_query ?: "N/A"}") + appendLine() appendLine("**Direct URLs:** ${executionConfig?.direct_urls?.joinToString(", ") ?: "N/A"}") - appendLine("**Content Queries:** ${executionConfig?.content_queries ?: "N/A"}") + appendLine() + appendLine("
Execution Configuration (click to expand)\n") + appendLine() + appendLine(executionConfig?.content_queries?.toJson()?.let { "\n```json\n${it.indent()}\n```" } ?: "N/A") + appendLine() + appendLine("
") appendLine() appendLine("---") appendLine() @@ -635,7 +641,6 @@ class CrawlerAgentTask( } } - fun addToQueue( newLink: LinkData, maxDepth: Int, maxQueueSize: Int ): Boolean = synchronized(pageQueueLock) { @@ -662,7 +667,9 @@ class CrawlerAgentTask( return false } seenUrls.add(newUrl) - pageQueue.add(newLink) + pageQueue.add(newLink.copy( + relevance_score = newLink.relevance_score.coerceIn(1.0, 100.0) + Random.nextInt(-500, 500) * 0.001 // Slight randomness to prevent priority ties + )) log.debug("Added new link to queue: $newUrl (depth=${newLink.depth}, priority=${newLink.calculatePriority()})") true } @@ -827,7 +834,7 @@ class CrawlerAgentTask( // Log page processing start to transcript transcriptStream?.let { stream -> try { - writeToTranscript(stream, "### Processing Page ${currentIndex}: [$title]($url)\n\n") + writeToTranscript(stream, "### Processing Page ${currentIndex}: [$title]($url) (priority=${"%0.3f".format(page.calculatePriority())})\n\n") writeToTranscript(stream, "**Started:** ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))}\n\n") } catch (e: Exception) { log.debug("Failed to write page start to transcript (stream may be closed)", e) @@ -1149,12 +1156,11 @@ class CrawlerAgentTask( private fun addPageQueueDetailsTab( tabs: TabbedDisplay, - task: SessionTask, processedCount: Int, errorCount: Int ) { try { - val queueDetailsTask = task.ui.newTask(false) + val queueDetailsTask = tabs.task.ui.newTask(false) tabs["Queue Details"] = queueDetailsTask.placeholder val queueDetails = buildString { appendLine("# Page Queue Details") @@ -1217,70 +1223,10 @@ class CrawlerAgentTask( } } appendLine() - // Depth distribution - appendLine("## Depth Distribution") - appendLine() - val depthCounts = unprocessedPages.groupBy { it.depth }.mapValues { it.value.size } - val maxDepth = (depthCounts.keys.maxOrNull() ?: 0) - if (depthCounts.isEmpty()) { - appendLine("*No depth data available.*") - } else { - appendLine("| Depth | Count | Percentage |") - appendLine("|-------|-------|------------|") - (0..maxDepth).forEach { depth -> - val count = depthCounts[depth] ?: 0 - val percentage = if (unprocessedPages.isNotEmpty()) { - (count * 100.0 / unprocessedPages.size).toInt() - } else 0 - appendLine("| $depth | $count | $percentage% |") - } - } - appendLine() - // Relevance distribution - appendLine("## Relevance Score Distribution") - appendLine() - val relevanceBuckets = unprocessedPages.groupBy { - ((it.relevance_score / 10).toInt() * 10).coerceIn(0, 100) - }.mapValues { it.value.size } - if (relevanceBuckets.isEmpty()) { - appendLine("*No relevance data available.*") - } else { - appendLine("| Score Range | Count | Percentage |") - appendLine("|-------------|-------|------------|") - (0..100 step 10).reversed().forEach { bucket -> - val count = relevanceBuckets[bucket] ?: 0 - val percentage = if (unprocessedPages.isNotEmpty()) { - (count * 100.0 / unprocessedPages.size).toInt() - } else 0 - val range = "${bucket}-${bucket + 9}" - appendLine("| $range | $count | $percentage% |") - } - } - appendLine() - // Top unprocessed pages by priority - appendLine("## Top 10 Unprocessed Pages by Priority") - appendLine() - val topPages = unprocessedPages.sortedByDescending { it.calculatePriority() }.take(10) - if (topPages.isEmpty()) { - appendLine("*No unprocessed pages.*") - } else { - topPages.forEachIndexed { index, page -> - appendLine("### ${index + 1}. [${page.title ?: "Untitled"}](${page.url})") - appendLine() - appendLine("- **URL:** ${page.url}") - appendLine("- **Relevance Score:** ${String.format("%.1f", page.relevance_score)}") - appendLine("- **Depth:** ${page.depth}") - appendLine("- **Priority:** ${String.format("%.2f", page.calculatePriority())}") - if (!page.tags.isNullOrEmpty()) { - appendLine("- **Tags:** ${page.tags.joinToString(", ")}") - } - appendLine() - } - } } } - queueDetailsTask.add(queueDetails.renderMarkdown) - task.update() + queueDetailsTask.complete(queueDetails.renderMarkdown) + tabs.task.update() log.info("Added page queue details tab with statistics") } catch (e: Exception) { log.error("Failed to create page queue details tab", e) diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/DataTableAccumulationStrategy.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/DataTableAccumulationStrategy.kt index 84ffa0286..db60bb315 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/DataTableAccumulationStrategy.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/DataTableAccumulationStrategy.kt @@ -1,10 +1,10 @@ package com.simiacryptus.cognotik.plan.tools.online.processing import com.simiacryptus.cognotik.agents.ParsedAgent +import com.simiacryptus.cognotik.agents.parserCast import com.simiacryptus.cognotik.describe.Description import com.simiacryptus.cognotik.plan.tools.online.CrawlerAgentTask import com.simiacryptus.cognotik.util.JsonUtil -import com.simiacryptus.cognotik.util.jsonCast import com.simiacryptus.cognotik.util.toJson import com.simiacryptus.cognotik.webui.session.getChildClient import org.slf4j.LoggerFactory @@ -82,11 +82,9 @@ class DataTableAccumulationStrategy : DefaultSummarizerStrategy() { log.debug("Processing page for data table accumulation: $url") val config = try { + val chatInterface = context.orchestrationConfig.parsingChatter.getChildClient(context.task) context.executionConfig.content_queries?.let { queries -> - when (queries) { - is String -> JsonUtil.fromJson(queries, DataTableConfig::class.java) - else -> queries.jsonCast() - } + queries.parserCast(chatInterface) } ?: run { log.warn("No data table config provided, using default") DataTableConfig() @@ -409,10 +407,7 @@ class DataTableAccumulationStrategy : DefaultSummarizerStrategy() { val config = try { context.executionConfig.content_queries?.let { queries -> - when (queries) { - is String -> JsonUtil.fromJson(queries, DataTableConfig::class.java) - else -> queries.jsonCast() - } + queries.parserCast(context.orchestrationConfig.parsingChatter.getChildClient(context.task)) } ?: DataTableConfig() } catch (e: Exception) { log.error("Failed to parse config for final output", e) diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/JobMatchingStrategy.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/JobMatchingStrategy.kt index 15c20bb7e..43cafdcb1 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/JobMatchingStrategy.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/JobMatchingStrategy.kt @@ -89,11 +89,11 @@ class JobMatchingStrategy : DefaultSummarizerStrategy() { @Description("Relocation assistance details") val relocation_assistance: String? = null, @Description("URL where the candidate can apply for the position") - val application_url: String = "", + val application_url: String? = null, @Description("URL of the original job description page") - val job_description_url: String = "", + val job_description_url: String? = null, @Description("Full text of the job description") - val job_description: String = "", + val job_description: String? = null, @Description("Minimum salary offered (if disclosed)") val salary_min: Int? = null, @Description("Maximum salary offered (if disclosed)") @@ -127,9 +127,9 @@ class JobMatchingStrategy : DefaultSummarizerStrategy() { @Description("Skills the candidate has that match the job requirements") val skill_matches: List = listOf(), @Description("Draft cover letter tailored to this specific position") - val cover_letter: String = "", + val cover_letter: String? = null, @Description("Strategic notes and recommendations for the application") - val application_notes: String = "" + val application_notes: String? = null ) private val goodMatches = ConcurrentHashMap() @@ -204,7 +204,7 @@ class JobMatchingStrategy : DefaultSummarizerStrategy() { val errorMsg = "Failed to analyze job match for URL: $url - ${e.message}" log.error(errorMsg, e) context.task.error(e) - writeToTranscript(context, "\n**ERROR:** $errorMsg\n```\n${e.stackTraceToString().indent(" ")}\n```\n\n") + writeToTranscript(context, "\n**ERROR:** $errorMsg\n") throw e } diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/SchemaExtractionStrategy.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/SchemaExtractionStrategy.kt index 56d9b1888..d9d606be3 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/SchemaExtractionStrategy.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/online/processing/SchemaExtractionStrategy.kt @@ -2,6 +2,7 @@ package com.simiacryptus.cognotik.plan.tools.online.processing import com.fasterxml.jackson.databind.ObjectMapper import com.simiacryptus.cognotik.agents.ParsedAgent +import com.simiacryptus.cognotik.agents.parserCast import com.simiacryptus.cognotik.describe.Description import com.simiacryptus.cognotik.plan.tools.online.CrawlerAgentTask import com.simiacryptus.cognotik.util.JsonUtil @@ -65,10 +66,7 @@ class SchemaExtractionStrategy : DefaultSummarizerStrategy() { log.debug("Processing page with schema extraction: $url") val config = try { context.executionConfig.content_queries?.let { queries -> - when (queries) { - is String -> JsonUtil.fromJson(queries, SchemaExtractionConfig::class.java) - else -> queries.jsonCast() - } + queries.parserCast(context.orchestrationConfig.parsingChatter.getChildClient(context.task)) } ?: run { log.warn("No schema extraction config provided, using default") SchemaExtractionConfig() @@ -268,12 +266,8 @@ class SchemaExtractionStrategy : DefaultSummarizerStrategy() { ): String { log.info("Generating final aggregated output") val config = try { - context.executionConfig.content_queries?.let { queries -> - when (queries) { - is String -> JsonUtil.fromJson(queries, SchemaExtractionConfig::class.java) - else -> queries.jsonCast() - } - } ?: SchemaExtractionConfig() + val chatInterface = context.orchestrationConfig.parsingChatter.getChildClient(context.task) + context.executionConfig.content_queries?.parserCast(chatInterface) ?: SchemaExtractionConfig() } catch (e: Exception) { log.error("Failed to parse config for final output", e) SchemaExtractionConfig() diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/reasoning/AbductiveReasoningTask.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/reasoning/AbductiveReasoningTask.kt index 2f9b9c2a9..2c3c428d6 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/reasoning/AbductiveReasoningTask.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/plan/tools/reasoning/AbductiveReasoningTask.kt @@ -202,7 +202,7 @@ AbductiveReasoning - Generate and evaluate explanatory hypotheses task.update() // Gather context - val priorContext = getPriorCode(agent.executionState) + val priorContext = getPriorCode(agent?.executionState) val combinedContext = (priorContext + "\n\n" + inputContext.joinToString("\n\n")).trim() if (priorContext.isNotBlank()) { log.debug("Found prior context: ${priorContext.length} characters") diff --git a/webui/src/main/kotlin/com/simiacryptus/cognotik/util/AddApplyFileDiffLinks.kt b/webui/src/main/kotlin/com/simiacryptus/cognotik/util/AddApplyFileDiffLinks.kt index 86f9773a8..f35654b07 100644 --- a/webui/src/main/kotlin/com/simiacryptus/cognotik/util/AddApplyFileDiffLinks.kt +++ b/webui/src/main/kotlin/com/simiacryptus/cognotik/util/AddApplyFileDiffLinks.kt @@ -107,7 +107,7 @@ open class AddApplyFileDiffLinks(val processor: PatchProcessor) { } protected open fun getInitiatorPattern(): Regex { - return "(?s)```\\w*\n".toRegex() + return processor.getInitiatorPattern() } protected open fun loadFile(filepath: Path?): String = try { @@ -134,7 +134,7 @@ open class AddApplyFileDiffLinks(val processor: PatchProcessor) { return "Patch Data" } - fun instrument( +fun instrument( self: SocketManager, root: Path, response: String, @@ -159,28 +159,17 @@ open class AddApplyFileDiffLinks(val processor: PatchProcessor) { ) } - val codeblockPattern = """(?s)(? findHeader(block, response) ?: defaultFile } - val findAllGreedy = codeblockGreedyPattern.findAll(response).toList() - .groupBy { block -> findHeader(block, response) ?: defaultFile } - val resolvedMatches = mutableListOf>>() - if (findAllGreedy.values.flatten().any { it.groupValues[1] == "markdown" }) { - - findAllGreedy.forEach { s, matchResults -> resolvedMatches.add(s to matchResults) } - } else { - - findAll.forEach { s, matchResults -> resolvedMatches.add(s to matchResults) } + val codeBlocks = processor.extractCodeBlocks(response) + val codeBlocksWithHeaders = codeBlocks.mapIndexed { index, (lang, code) -> + val header = findHeaderForBlock(response, lang, code, index) ?: defaultFile + Triple(header, lang, code) } val headerPattern = """(? + val newFileBlocks = codeBlocksWithHeaders.filter { (header, lang, code) -> try { val resolvedPath = resolver(root, header ?: return@filter false) resolvedPath == null || !root.resolve(resolvedPath).toFile().exists() @@ -188,9 +177,9 @@ open class AddApplyFileDiffLinks(val processor: PatchProcessor) { log.info("Error processing code block", e) false } - }.flatMap { it.second }.map { it.range to it }.toList() + } - val patchBlocks = resolvedMatches.filter { (header, block) -> + val patchBlocks = codeBlocksWithHeaders.filter { (header, lang, code) -> try { val resolvedPath = resolver(root, header ?: return@filter false) resolvedPath != null && root.resolve(resolvedPath).toFile().exists() @@ -198,43 +187,42 @@ open class AddApplyFileDiffLinks(val processor: PatchProcessor) { log.info("Error processing code block", e) false } - }.flatMap { it.second }.map { it.range to it }.toList() + } - val withPatchLinks: String = patchBlocks.foldIndexed(response) { index, markdown, diffBlock -> - val diffValue = diffBlock.second.groupValues[2].trim() - val header = headers.lastOrNull { it.first.last < diffBlock.first.first }?.second ?: defaultFile ?: "Unknown" - val filename = resolver(root, normalizeFilename(header)) + val withPatchLinks: String = patchBlocks.foldIndexed(response) { index, markdown, (header, lang, diffValue) -> + val filename = resolver(root, normalizeFilename(header ?: "")) if (filename.isNullOrBlank()) return@foldIndexed markdown val newValue = renderDiffBlock(root, filename, diffValue, handle, self, shouldAutoApply) - markdown.replace(diffBlock.second.value, newValue) + markdown.replace("```$lang\n$diffValue\n```", newValue) } - val withSaveLinks = codeblocks.foldIndexed(withPatchLinks) { index, markdown, codeBlock -> - val lang = codeBlock.second.groupValues[1] - var codeValue = codeBlock.second.groupValues[2].trim().trimIndent() + val withSaveLinks = newFileBlocks.foldIndexed(withPatchLinks) { index, markdown, (header, lang, codeValue) -> + var processedCode = codeValue.trimIndent() if (codeValue.lines().all { it.startsWith('+') || it.startsWith('-') }) { - codeValue = codeValue.lines().joinToString("\n") { it.drop(1) } + processedCode = codeValue.lines().joinToString("\n") { it.drop(1) } } - val header = headers.lastOrNull { it.first.last < codeBlock.first.first }?.second ?: defaultFile if (header.isNullOrBlank()) return markdown val filename = prefilterFilename(normalizeFilename(header)) if (filename.isNullOrBlank()) return markdown - val newMarkdown = renderNewFile(root, filename, codeValue, handle, self, lang, shouldAutoApply) + record( + val newMarkdown = renderNewFile(root, filename, processedCode, handle, self, lang, shouldAutoApply) + record( self, mapOf( "filename" to filename, - "code" to codeValue, + "code" to processedCode, "header" to header, "language" to lang, ) ) - markdown.replace(codeBlock.second.value, newMarkdown) + markdown.replace("```$lang\n$codeValue\n```", newMarkdown) } return withSaveLinks } } - private fun findHeader(block: MatchResult, response: String): String? { + private fun findHeaderForBlock(response: String, lang: String, code: String, blockIndex: Int): String? { + val blockText = "```$lang\n$code\n```" + val blockPosition = response.indexOf(blockText) + if (blockPosition == -1) return null val markdownHeaderPattern = """(? headers.add(match.range to normalizeFilename(match.groupValues[1])) } - return headers.filter { it.first.last <= block.range.first } + return headers.filter { it.first.last <= blockPosition } .maxByOrNull { it.first.last }?.second } diff --git a/webui/src/main/resources/presentations/presentation.css b/webui/src/main/resources/presentations/presentation.css index aa1f5d26d..40b9b6753 100644 --- a/webui/src/main/resources/presentations/presentation.css +++ b/webui/src/main/resources/presentations/presentation.css @@ -33,8 +33,12 @@ } .reveal .slides section { - height: 100%; - padding: 20px; + height: auto; + min-height: 100vh; + padding: clamp(1rem, 3vw, 2rem); + display: flex; + flex-direction: column; + justify-content: center; } .reveal .progress {