diff --git a/build.gradle.kts b/build.gradle.kts
index 3a028cfe..9149c457 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,7 +3,7 @@ group = properties("libraryGroup")
version = properties("libraryVersion")
//plugins {
-// id("org.jetbrains.kotlin.jvm") version "1.9.21"
+// id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5"
//}
tasks {
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 101e4712..2cdada91 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -8,7 +8,7 @@ version = properties("libraryVersion")
plugins {
java
`java-library`
- id("org.jetbrains.kotlin.jvm") version "1.9.21"
+ id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5"
`maven-publish`
id("signing")
}
diff --git a/gradle.properties b/gradle.properties
index 41430921..e82aa896 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
# Gradle Releases -> https://github.com/gradle/gradle/releases
libraryGroup = com.simiacryptus.skyenet
-libraryVersion = 1.0.60
+libraryVersion = 1.0.61
gradleVersion = 7.6.1
diff --git a/groovy/build.gradle.kts b/groovy/build.gradle.kts
index 0fd09d5d..10a6708c 100644
--- a/groovy/build.gradle.kts
+++ b/groovy/build.gradle.kts
@@ -8,7 +8,7 @@ version = properties("libraryVersion")
plugins {
java
`java-library`
- id("org.jetbrains.kotlin.jvm") version "1.9.21"
+ id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5"
`maven-publish`
id("signing")
}
@@ -26,7 +26,7 @@ kotlin {
jvmToolchain(11)
}
-val kotlin_version = "1.9.21"
+val kotlin_version = "2.0.0-Beta5"
dependencies {
implementation(project(":core"))
diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts
index 71d16f5a..403a1199 100644
--- a/kotlin/build.gradle.kts
+++ b/kotlin/build.gradle.kts
@@ -8,7 +8,7 @@ version = properties("libraryVersion")
plugins {
java
`java-library`
- id("org.jetbrains.kotlin.jvm") version "1.9.21"
+ id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5"
`maven-publish`
id("signing")
}
diff --git a/scala/build.gradle.kts b/scala/build.gradle.kts
index 6d0ce9d0..7bd4308b 100644
--- a/scala/build.gradle.kts
+++ b/scala/build.gradle.kts
@@ -10,7 +10,7 @@ plugins {
`java-library`
`scala`
`maven-publish`
- id("org.jetbrains.kotlin.jvm") version "1.9.21"
+ id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5"
id("signing")
}
diff --git a/webui/build.gradle.kts b/webui/build.gradle.kts
index 3e218d6f..fa367a0b 100644
--- a/webui/build.gradle.kts
+++ b/webui/build.gradle.kts
@@ -9,7 +9,7 @@ version = properties("libraryVersion")
plugins {
java
`java-library`
- id("org.jetbrains.kotlin.jvm") version "1.9.21"
+ id("org.jetbrains.kotlin.jvm") version "2.0.0-Beta5"
`maven-publish`
id("signing")
id("io.freefair.sass-base") version "8.4"
@@ -30,7 +30,7 @@ kotlin {
// jvmToolchain(17)
}
-val kotlin_version = "1.9.21"
+val kotlin_version = "2.0.0-Beta5"
val jetty_version = "11.0.18"
val jackson_version = "2.15.3"
dependencies {
diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt
new file mode 100644
index 00000000..b4b6732f
--- /dev/null
+++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyDiffLinks.kt
@@ -0,0 +1,77 @@
+package com.github.simiacryptus.aicoder.util
+
+import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs
+import com.simiacryptus.skyenet.webui.application.ApplicationInterface
+import com.simiacryptus.skyenet.webui.session.SessionTask
+import com.simiacryptus.skyenet.webui.session.SocketManagerBase
+import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown
+
+fun SocketManagerBase.addApplyDiffLinks(
+ code: StringBuilder,
+ response: String,
+ handle: (String) -> Unit,
+ task: SessionTask,
+ ui: ApplicationInterface? = null,
+): String {
+ val diffPattern = """(?s)(?
+ val diffVal: String = diffBlock.groupValues[1]
+ val hrefLink = hrefLink("Apply Diff") {
+ try {
+ val newCode = PatchUtil.patch(code.toString(), diffVal).replace("\r", "")
+ handle(newCode)
+ task.complete("""
Diff Applied
""")
+ } catch (e: Throwable) {
+ task.error(ui, e)
+ }
+ }
+ val reverseHrefLink = hrefLink("(Bottom to Top)") {
+ try {
+ val reversedCode = code.lines().reversed().joinToString("\n")
+ val reversedDiff = diffVal.lines().reversed().joinToString("\n")
+ val newReversedCode = PatchUtil.patch(reversedCode, reversedDiff).replace("\r", "")
+ val newCode = newReversedCode.lines().reversed().joinToString("\n")
+ handle(newCode)
+ task.complete("""Diff Applied (Bottom to Top)
""")
+ } catch (e: Throwable) {
+ task.error(ui, e)
+ }
+ }
+ val patch = PatchUtil.patch(code.toString(), diffVal).replace("\r", "")
+ val test1 = DiffUtil.formatDiff(
+ DiffUtil.generateDiff(
+ code.toString().replace("\r", "").lines(),
+ patch.lines()
+ )
+ )
+ val patchRev = PatchUtil.patch(
+ code.lines().reversed().joinToString("\n"),
+ diffVal.lines().reversed().joinToString("\n")
+ ).replace("\r", "")
+ val test2 = DiffUtil.formatDiff(
+ DiffUtil.generateDiff(
+ code.lines(),
+ patchRev.lines().reversed()
+ )
+ )
+ val newValue = if (patchRev == patch) {
+ displayMapInTabs(
+ mapOf(
+ "Diff" to renderMarkdown("```diff\n$diffVal\n```", ui = ui, tabs = true),
+ "Verify" to renderMarkdown("```diff\n$test1\n```", ui = ui, tabs = true),
+ ), ui = ui, split = true
+ ) + "\n" + hrefLink
+ } else {
+ displayMapInTabs(
+ mapOf(
+ "Diff" to renderMarkdown("```diff\n$diffVal\n```", ui = ui, tabs = true),
+ "Verify" to renderMarkdown("```diff\n$test1\n```", ui = ui, tabs = true),
+ "Reverse" to renderMarkdown("```diff\n$test2\n```", ui = ui, tabs = true),
+ ), ui = ui, split = true
+ ) + "\n" + hrefLink + "\n" + reverseHrefLink
+ }
+ markdown.replace(diffBlock.value, newValue)
+ }
+ return withLinks
+}
\ No newline at end of file
diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt
new file mode 100644
index 00000000..c9771fca
--- /dev/null
+++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddApplyFileDiffLinks.kt
@@ -0,0 +1,249 @@
+package com.github.simiacryptus.aicoder.util
+
+import com.simiacryptus.skyenet.AgentPatterns
+import com.simiacryptus.skyenet.webui.application.ApplicationInterface
+import com.simiacryptus.skyenet.webui.session.SessionTask
+import com.simiacryptus.skyenet.webui.session.SocketManagerBase
+import com.simiacryptus.skyenet.webui.util.MarkdownUtil
+import java.io.File
+import java.nio.file.Path
+import java.util.concurrent.TimeUnit
+import kotlin.io.path.readText
+
+fun SocketManagerBase.addApplyFileDiffLinks(
+ root: Path,
+ code: Map,
+ response: String,
+ handle: (Map) -> Unit,
+ task: SessionTask,
+ ui: ApplicationInterface,
+): String {
+ val headerPattern = """(?s)(?> = diffPattern.findAll(response).map { it.range to it.groupValues[1] }.toList()
+ val codeblocks = codeblockPattern.findAll(response).filter {
+ when (it.groupValues[1]) {
+ "diff" -> false
+ else -> true
+ }
+ }.map { it.range to it }.toList()
+ val withPatchLinks: String = diffs.fold(response) { markdown, diffBlock ->
+ val header = headers.lastOrNull { it.first.endInclusive < diffBlock.first.start }
+ val filename = header?.second ?: "Unknown"
+ val diffVal = diffBlock.second
+ val newValue = renderDiffBlock(root, filename, code, diffVal, handle, task, ui)
+ markdown.replace(diffVal, newValue)
+ }
+ val withSaveLinks = codeblocks.fold(withPatchLinks) { markdown, codeBlock ->
+ val header = headers.lastOrNull { it.first.endInclusive < codeBlock.first.start }
+ val filename = header?.second ?: "Unknown"
+ val filepath = path(root, filename)
+ val prevCode = load(filepath, root, code)
+ val codeLang = codeBlock.second.groupValues[1]
+ val codeValue = codeBlock.second.groupValues[2]
+ val hrefLink = hrefLink("Save File") {
+ try {
+ handle(
+ mapOf(
+ filename to codeValue
+ )
+ )
+ task.complete("""Saved ${filename}
""")
+ } catch (e: Throwable) {
+ task.error(null, e)
+ }
+ }
+ val codeblockRaw = """
+ ```${codeLang}
+ ${codeValue}
+ ```
+ """.trimIndent()
+ markdown.replace(
+ codeblockRaw, AgentPatterns.displayMapInTabs(
+ mapOf(
+ "New" to MarkdownUtil.renderMarkdown(codeblockRaw, ui = ui),
+ "Old" to MarkdownUtil.renderMarkdown(
+ """
+ |```${codeLang}
+ |${prevCode}
+ |```
+ """.trimMargin(), ui = ui
+ ),
+ "Patch" to MarkdownUtil.renderMarkdown(
+ """
+ |```diff
+ |${
+ DiffUtil.formatDiff(
+ DiffUtil.generateDiff(
+ prevCode.lines(),
+ codeValue.lines()
+ )
+ )
+ }
+ |```
+ """.trimMargin(), ui = ui
+ ),
+ )
+ ) + "\n" + hrefLink
+ )
+ }
+ return withSaveLinks
+}
+
+
+private fun SocketManagerBase.renderDiffBlock(
+ root: Path,
+ filename: String,
+ code: Map,
+ diffVal: String,
+ handle: (Map) -> Unit,
+ task: SessionTask,
+ ui: ApplicationInterface
+): String {
+ val filepath = path(root, filename)
+ val prevCode = load(filepath, root, code)
+ val newCode = PatchUtil.patch(prevCode, diffVal)
+ val echoDiff = try {
+ DiffUtil.formatDiff(
+ DiffUtil.generateDiff(
+ prevCode.lines(),
+ newCode.lines()
+ )
+ )
+ } catch (e: Throwable) {
+ MarkdownUtil.renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui)
+ }
+
+ val hrefLink = hrefLink("Apply Diff") {
+ try {
+ val relativize = try {
+ root.relativize(filepath)
+ } catch (e: Throwable) {
+ filepath
+ }
+ handle(
+ mapOf(
+ relativize.toString() to PatchUtil.patch(
+ prevCode,
+ diffVal
+ )
+ )
+ )
+ task.complete("""Diff Applied
""")
+ } catch (e: Throwable) {
+ task.error(null, e)
+ }
+ }
+ val reverseHrefLink = hrefLink("(Bottom to Top)") {
+ try {
+ val reversedCodeMap = code.mapValues { (_, v) -> v.lines().reversed().joinToString("\n") }
+ val reversedDiff = diffVal.lines().reversed().joinToString("\n")
+ val newReversedCodeMap = reversedCodeMap.mapValues { (file, prevCode) ->
+ if (filename == file) {
+ PatchUtil.patch(prevCode, reversedDiff).lines().reversed().joinToString("\n")
+ } else prevCode
+ }
+ handle(newReversedCodeMap)
+ task.complete("""Diff Applied (Bottom to Top)
""")
+ } catch (e: Throwable) {
+ task.error(null, e)
+ }
+ }
+ val diffTask = ui?.newTask(root = false)
+ val prevCodeTask = ui?.newTask(root = false)
+ val newCodeTask = ui?.newTask(root = false)
+ val patchTask = ui?.newTask(root = false)
+ val inTabs = AgentPatterns.displayMapInTabs(
+ mapOf(
+ "Diff" to (diffTask?.placeholder ?: ""),
+ "Code" to (prevCodeTask?.placeholder ?: ""),
+ "Preview" to (newCodeTask?.placeholder ?: ""),
+ "Echo" to (patchTask?.placeholder ?: ""),
+ )
+ )
+ SocketManagerBase.scheduledThreadPoolExecutor.schedule({
+ diffTask?.add(MarkdownUtil.renderMarkdown(/*escapeHtml4*/(diffVal), ui = ui))
+ newCodeTask?.add(
+ MarkdownUtil.renderMarkdown(
+ "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```",
+ ui = ui
+ )
+ )
+ prevCodeTask?.add(
+ MarkdownUtil.renderMarkdown(
+ "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```",
+ ui = ui
+ )
+ )
+ patchTask?.add(MarkdownUtil.renderMarkdown("# $filename\n\n```diff\n ${echoDiff}\n```", ui = ui))
+ }, 100, TimeUnit.MILLISECONDS)
+ val newValue = inTabs + "\n" + hrefLink + "\n" + reverseHrefLink
+ return newValue
+}
+
+
+private fun load(
+ filepath: Path?,
+ root: Path,
+ code: Map
+) = try {
+ if (true != filepath?.toFile()?.exists()) {
+ log.warn(
+ """
+ |File not found: $filepath
+ |Root: ${root.toAbsolutePath()}
+ |Files:
+ |${code.keys.joinToString("\n") { "* $it" }}
+ """.trimMargin()
+ )
+ ""
+ } else {
+ filepath.readText(Charsets.UTF_8)
+ }
+} catch (e: Throwable) {
+ log.error("Error reading file: $filepath", e)
+ ""
+}
+
+private fun path(root: Path, filename: String): Path? {
+ val filepath = try {
+ findFile(root, filename) ?: root.resolve(filename)
+ } catch (e: Throwable) {
+ log.error("Error finding file: $filename", e)
+ try {
+ root.resolve(filename)
+ } catch (e: Throwable) {
+ log.error("Error resolving file: $filename", e)
+ File(filename).toPath()
+ }
+ }
+ return filepath
+}
+
+fun findFile(root: Path, filename: String): Path? {
+ return try {
+ when {
+ /* filename is absolute */
+ filename.startsWith("/") -> {
+ val resolve = File(filename)
+ if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix("/"))
+ }
+ /* win absolute */
+ filename.indexOf(":\\") == 1 -> {
+ val resolve = File(filename)
+ if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix(filename.substring(0, 2)))
+ }
+
+ root.resolve(filename).toFile().exists() -> root.resolve(filename)
+ null != root.parent && root != root.parent -> findFile(root.parent, filename)
+ else -> null
+ }
+ } catch (e: Throwable) {
+ log.error("Error finding file: $filename", e)
+ null
+ }
+}
+
+private val log = org.slf4j.LoggerFactory.getLogger(PatchUtil::class.java)
\ No newline at end of file
diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt
new file mode 100644
index 00000000..c46f8e8b
--- /dev/null
+++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/AddSaveLinks.kt
@@ -0,0 +1,28 @@
+package com.github.simiacryptus.aicoder.util
+
+import com.simiacryptus.skyenet.webui.session.SessionTask
+import com.simiacryptus.skyenet.webui.session.SocketManagerBase
+
+fun SocketManagerBase.addSaveLinks(
+ response: String,
+ task: SessionTask,
+ handle: (String, String) -> Unit
+): String {
+ val diffPattern =
+ """(?s)(?
+ val filename = diffBlock.groupValues[1]
+ val codeValue = diffBlock.groupValues[2]
+ val hrefLink = hrefLink("Save File") {
+ try {
+ handle(filename, codeValue)
+ task.complete("""Saved ${filename}
""")
+ } catch (e: Throwable) {
+ task.error(null, e)
+ }
+ }
+ markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + "```\n" + hrefLink)
+ }
+ return withLinks
+}
\ No newline at end of file
diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt
index 232a7e6e..23b0d304 100644
--- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt
+++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtil.kt
@@ -1,19 +1,7 @@
package com.github.simiacryptus.aicoder.util
-import com.github.simiacryptus.aicoder.util.ApxPatchUtil.patch
-import com.github.simiacryptus.aicoder.util.DiffUtil.formatDiff
-import com.github.simiacryptus.aicoder.util.DiffUtil.generateDiff
-import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs
-import com.simiacryptus.skyenet.webui.application.ApplicationInterface
-import com.simiacryptus.skyenet.webui.session.SessionTask
-import com.simiacryptus.skyenet.webui.session.SocketManagerBase
-import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown
import org.apache.commons.text.similarity.LevenshteinDistance
import org.slf4j.LoggerFactory
-import java.io.File
-import java.nio.file.Path
-import java.util.concurrent.TimeUnit
-import kotlin.io.path.readText
object ApxPatchUtil {
@@ -122,10 +110,10 @@ object ApxPatchUtil {
private fun lineMatches(
a: String,
b: String,
- factor: Double = 0.3
+ factor: Double = 0.1,
): Boolean {
val threshold = (Math.max(a.trim().length, b.trim().length) * factor).toInt()
- val levenshteinDistance = LevenshteinDistance(5)
+ val levenshteinDistance = LevenshteinDistance(threshold+1)
val dist = levenshteinDistance.apply(a.trim(), b.trim())
return if (dist >= 0) {
dist <= threshold
@@ -135,321 +123,5 @@ object ApxPatchUtil {
}
}
-fun SocketManagerBase.addApplyDiffLinks(
- code: StringBuilder,
- response: String,
- handle: (String) -> Unit,
- task: SessionTask,
- ui: ApplicationInterface? = null,
-): String {
- val diffPattern = """(?s)(?
- val diffVal = diffBlock.value
- val hrefLink = hrefLink("Apply Diff") {
- try {
- val newCode = patch(code.toString(), diffVal)
- handle(newCode)
- task.complete("""Diff Applied
""")
- } catch (e: Throwable) {
- task.error(ui, e)
- }
- }
- val reverseHrefLink = hrefLink("(Bottom to Top)") {
- try {
- val reversedCode = code.lines().reversed().joinToString("\n")
- val reversedDiff = diffVal.lines().reversed().joinToString("\n")
- val newReversedCode = patch(reversedCode, reversedDiff)
- val newCode = newReversedCode.lines().reversed().joinToString("\n")
- handle(newCode)
- task.complete("""Diff Applied (Bottom to Top)
""")
- } catch (e: Throwable) {
- task.error(ui, e)
- }
- }
- val test1 = formatDiff(
- generateDiff(
- code.lines(),
- patch(code.toString(), diffVal).lines()
- )
- )
- val test2 = formatDiff(
- generateDiff(
- code.lines(),
- patch(
- code.lines().reversed().joinToString("\n"),
- diffVal.lines().reversed().joinToString("\n")
- ).lines().reversed()
- )
- )
- val newValue = displayMapInTabs(
- mapOf(
- "Diff" to renderMarkdown(diffVal, ui = ui, tabs = true),
- "Verify" to renderMarkdown("```diff\n$test1\n```", ui = ui, tabs = true),
- "Reverse" to renderMarkdown("```diff\n$test2\n```", ui = ui, tabs = true),
- ), ui = ui
- ) + "\n" + hrefLink + "\n" + reverseHrefLink
- markdown.replace(diffVal, newValue)
- }
- return withLinks
-}
-
-fun SocketManagerBase.addSaveLinks(
- response: String,
- task: SessionTask,
- handle: (String, String) -> Unit
-): String {
- val diffPattern =
- """(?s)(?
- val filename = diffBlock.groupValues[1]
- val codeValue = diffBlock.groupValues[2]
- val hrefLink = hrefLink("Save File") {
- try {
- handle(filename, codeValue)
- task.complete("""Saved ${filename}
""")
- } catch (e: Throwable) {
- task.error(null, e)
- }
- }
- markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + "```\n" + hrefLink)
- }
- return withLinks
-}
-
private val log = LoggerFactory.getLogger(ApxPatchUtil::class.java)
-fun findFile(root: Path, filename: String): Path? {
- return try {
- when {
- /* filename is absolute */
- filename.startsWith("/") -> {
- val resolve = File(filename)
- if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix("/"))
- }
- /* win absolute */
- filename.indexOf(":\\") == 1 -> {
- val resolve = File(filename)
- if (resolve.exists()) resolve.toPath() else findFile(root, filename.removePrefix(filename.substring(0, 2)))
- }
-
- root.resolve(filename).toFile().exists() -> root.resolve(filename)
- null != root.parent && root != root.parent -> findFile(root.parent, filename)
- else -> null
- }
- } catch (e: Throwable) {
- log.error("Error finding file: $filename", e)
- null
- }
-}
-
-fun SocketManagerBase.addApplyDiffLinks2(
- root: Path,
- code: Map,
- response: String,
- handle: (Map) -> Unit,
- task: SessionTask,
- ui: ApplicationInterface,
-): String {
- val headerPattern = """(?s)(?> = diffPattern.findAll(response).map { it.range to it.groupValues[1] }.toList()
- val codeblocks = codeblockPattern.findAll(response).filter {
- when (it.groupValues[1]) {
- "diff" -> false
- else -> true
- }
- }.map { it.range to it }.toList()
- val withPatchLinks: String = diffs.fold(response) { markdown, diffBlock ->
- val header = headers.lastOrNull { it.first.endInclusive < diffBlock.first.start }
- val filename = header?.second ?: "Unknown"
- val diffVal = diffBlock.second
- val newValue = renderDiffBlock(root, filename, code, diffVal, handle, task, ui)
- markdown.replace(diffVal, newValue)
- }
- val withSaveLinks = codeblocks.fold(withPatchLinks) { markdown, codeBlock ->
- val header = headers.lastOrNull { it.first.endInclusive < codeBlock.first.start }
- val filename = header?.second ?: "Unknown"
- val filepath = path(root, filename)
- val prevCode = load(filepath, root, code)
- val codeLang = codeBlock.second.groupValues[1]
- val codeValue = codeBlock.second.groupValues[2]
- val hrefLink = hrefLink("Save File") {
- try {
- handle(
- mapOf(
- filename to codeValue
- )
- )
- task.complete("""Saved ${filename}
""")
- } catch (e: Throwable) {
- task.error(null, e)
- }
- }
- val codeblockRaw = """
- ```${codeLang}
- ${codeValue}
- ```
- """.trimIndent()
- markdown.replace(
- codeblockRaw, displayMapInTabs(
- mapOf(
- "New" to renderMarkdown(codeblockRaw, ui = ui),
- "Old" to renderMarkdown(
- """
- |```${codeLang}
- |${prevCode}
- |```
- """.trimMargin(), ui = ui
- ),
- "Patch" to renderMarkdown(
- """
- |```diff
- |${
- formatDiff(
- generateDiff(
- prevCode.lines(),
- codeValue.lines()
- )
- )
- }
- |```
- """.trimMargin(), ui = ui
- ),
- )
- ) + "\n" + hrefLink
- )
- }
- return withSaveLinks
-}
-
-private fun SocketManagerBase.renderDiffBlock(
- root: Path,
- filename: String,
- code: Map,
- diffVal: String,
- handle: (Map) -> Unit,
- task: SessionTask,
- ui: ApplicationInterface
-): String {
- val filepath = path(root, filename)
- val prevCode = load(filepath, root, code)
- val newCode = patch(prevCode, diffVal)
- val echoDiff = try {
- formatDiff(
- generateDiff(
- prevCode.lines(),
- newCode.lines()
- )
- )
- } catch (e: Throwable) {
- renderMarkdown("```\n${e.stackTraceToString()}\n```", ui = ui)
- }
-
- val hrefLink = hrefLink("Apply Diff") {
- try {
- val relativize = try {
- root.relativize(filepath)
- } catch (e: Throwable) {
- filepath
- }
- handle(
- mapOf(
- relativize.toString() to patch(
- prevCode,
- diffVal
- )
- )
- )
- task.complete("""Diff Applied
""")
- } catch (e: Throwable) {
- task.error(null, e)
- }
- }
- val reverseHrefLink = hrefLink("(Bottom to Top)") {
- try {
- val reversedCodeMap = code.mapValues { (_, v) -> v.lines().reversed().joinToString("\n") }
- val reversedDiff = diffVal.lines().reversed().joinToString("\n")
- val newReversedCodeMap = reversedCodeMap.mapValues { (file, prevCode) ->
- if (filename == file) {
- patch(prevCode, reversedDiff).lines().reversed().joinToString("\n")
- } else prevCode
- }
- handle(newReversedCodeMap)
- task.complete("""Diff Applied (Bottom to Top)
""")
- } catch (e: Throwable) {
- task.error(null, e)
- }
- }
- val diffTask = ui?.newTask(root = false)
- val prevCodeTask = ui?.newTask(root = false)
- val newCodeTask = ui?.newTask(root = false)
- val patchTask = ui?.newTask(root = false)
- val inTabs = displayMapInTabs(
- mapOf(
- "Diff" to (diffTask?.placeholder ?: ""),
- "Code" to (prevCodeTask?.placeholder ?: ""),
- "Preview" to (newCodeTask?.placeholder ?: ""),
- "Echo" to (patchTask?.placeholder ?: ""),
- )
- )
- SocketManagerBase.scheduledThreadPoolExecutor.schedule({
- diffTask?.add(renderMarkdown(/*escapeHtml4*/(diffVal), ui = ui))
- newCodeTask?.add(
- renderMarkdown(
- "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```",
- ui = ui
- )
- )
- prevCodeTask?.add(
- renderMarkdown(
- "# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```",
- ui = ui
- )
- )
- patchTask?.add(renderMarkdown("# $filename\n\n```diff\n ${echoDiff}\n```", ui = ui))
- }, 100, TimeUnit.MILLISECONDS)
- val newValue = inTabs + "\n" + hrefLink + "\n" + reverseHrefLink
- return newValue
-}
-
-private fun load(
- filepath: Path?,
- root: Path,
- code: Map
-) = try {
- if (true != filepath?.toFile()?.exists()) {
- log.warn(
- """
- |File not found: $filepath
- |Root: ${root.toAbsolutePath()}
- |Files:
- |${code.keys.joinToString("\n") { "* $it" }}
- """.trimMargin()
- )
- ""
- } else {
- filepath.readText(Charsets.UTF_8)
- }
-} catch (e: Throwable) {
- log.error("Error reading file: $filepath", e)
- ""
-}
-
-private fun path(root: Path, filename: String): Path? {
- val filepath = try {
- findFile(root, filename) ?: root.resolve(filename)
- } catch (e: Throwable) {
- log.error("Error finding file: $filename", e)
- try {
- root.resolve(filename)
- } catch (e: Throwable) {
- log.error("Error resolving file: $filename", e)
- File(filename).toPath()
- }
- }
- return filepath
-}
\ No newline at end of file
diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt
new file mode 100644
index 00000000..16d20ffe
--- /dev/null
+++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtil.kt
@@ -0,0 +1,196 @@
+package com.github.simiacryptus.aicoder.util
+
+import org.apache.commons.text.similarity.LevenshteinDistance
+import org.slf4j.LoggerFactory
+
+object IterativePatchUtil {
+
+ enum class LineType { CONTEXT, ADD, DELETE }
+ class LineRecord(
+ val index: Int,
+ val line: String,
+ val previousLine: LineRecord? = null,
+ val nextLine: LineRecord? = null,
+ var matchingLine: LineRecord? = null,
+ var type: LineType = LineType.CONTEXT
+ )
+
+ fun patch(source: String, patch: String): String {
+ val sourceLines = parseLines(source)
+ val patchLines = parsePatchLines(patch)
+
+ // Step 1: Link all unique lines in the source and patch that match exactly
+ linkUniqueMatchingLines(sourceLines, patchLines)
+
+ // Step 2: Link all exact matches in the source and patch which are adjacent to established links
+ linkAdjacentMatchingLines(sourceLines, patchLines)
+
+ // Step 3: Establish a distance metric for matches based on Levenshtein distance and distance to established links.
+ // Use this to establish the links based on a shortest-first policy and iterate until no more good matches are found.
+ linkByLevenshteinDistance(sourceLines, patchLines)
+
+ // Generate the patched text
+ return generatePatchedText(sourceLines, patchLines)
+ }
+
+ private fun generatePatchedText(sourceLines: List, patchLines: List): String {
+ val patchedTextBuilder = StringBuilder()
+
+ // Add unmatched code lines at the beginning
+ sourceLines
+ .takeWhile { it.matchingLine == null }
+ .forEach { patchedTextBuilder.appendln(it.line) }
+
+ // Iterate through patch lines and apply changes to the source
+ patchLines.forEach { patchLine ->
+ when (patchLine.type) {
+ LineType.ADD -> patchedTextBuilder.appendln(patchLine.line)
+ LineType.DELETE -> {
+ // Skip adding the line to the patched text
+ }
+
+ LineType.CONTEXT -> {
+ // For context lines, we need to find the corresponding line in the source
+ // If there's a matching line, we add the source line to maintain original formatting
+ // If not, we add the patch line (it could be a case where context lines don't match exactly due to trimming)
+ val sourceLine = sourceLines.find { it.index == patchLine.index && it.matchingLine == patchLine }
+ if (sourceLine != null) {
+ patchedTextBuilder.appendln(sourceLine.line)
+ } else {
+ patchedTextBuilder.appendln(patchLine.line)
+ }
+ }
+ }
+ }
+
+ // Add unmatched code lines at the end
+ sourceLines.reversed()
+ .takeWhile { it.matchingLine == null }
+ .reversed()
+ .forEach { patchedTextBuilder.appendln(it.line) }
+
+ return patchedTextBuilder.toString().trimEnd()
+ }
+
+ private fun linkUniqueMatchingLines(sourceLines: List, patchLines: List) {
+ val sourceLineMap = sourceLines.groupBy { it.line.trim() }
+ val patchLineMap = patchLines.groupBy { it.line.trim() }
+
+ sourceLineMap.keys.intersect(patchLineMap.keys).forEach { key ->
+ val sourceLine = sourceLineMap[key]?.singleOrNull()
+ val patchLine = patchLineMap[key]?.singleOrNull()
+ if (sourceLine != null && patchLine != null) {
+ sourceLine.matchingLine = patchLine
+ patchLine.matchingLine = sourceLine
+ }
+ }
+ }
+
+ private fun linkAdjacentMatchingLines(sourceLines: List, patchLines: List) {
+ var foundMatch = true
+ while (foundMatch) {
+ foundMatch = false
+ for (sourceLine in sourceLines) {
+ val patchLine = sourceLine.matchingLine ?: continue // Skip if there's no matching line
+
+ // Check the previous line
+ if (sourceLine.previousLine != null && patchLine.previousLine != null) {
+ val sourcePrev = sourceLine.previousLine
+ val patchPrev = patchLine.previousLine
+ if (sourcePrev.line.trim() == patchPrev.line.trim() && sourcePrev.matchingLine == null && patchPrev.matchingLine == null) {
+ sourcePrev.matchingLine = patchPrev
+ patchPrev.matchingLine = sourcePrev
+ foundMatch = true
+ }
+ }
+
+ // Check the next line
+ if (sourceLine.nextLine != null && patchLine.nextLine != null) {
+ val sourceNext = sourceLine.nextLine
+ val patchNext = patchLine.nextLine
+ if (sourceNext.line.trim() == patchNext.line.trim() && sourceNext.matchingLine == null && patchNext.matchingLine == null) {
+ sourceNext.matchingLine = patchNext
+ patchNext.matchingLine = sourceNext
+ foundMatch = true
+ }
+ }
+ }
+ }
+ }
+
+ private fun linkByLevenshteinDistance(sourceLines: List, patchLines: List) {
+ val levenshteinDistance = LevenshteinDistance()
+ val maxDistance = 5 // Define a maximum acceptable distance. Adjust as needed.
+
+ // Iterate over source lines to find potential matches in the patch lines
+ for (sourceLine in sourceLines) {
+ if (sourceLine.matchingLine != null) continue // Skip lines that already have matches
+
+ var bestMatch: LineRecord? = null
+ var bestDistance = Int.MAX_VALUE
+ var bestCombinedDistance = Int.MAX_VALUE
+
+ for (patchLine in patchLines) {
+ if (patchLine.matchingLine != null) continue // Skip lines that already have matches
+
+ val distance = levenshteinDistance.apply(sourceLine.line.trim(), patchLine.line.trim())
+ if (distance <= maxDistance) {
+ // Calculate combined distance, factoring in proximity to established links
+ val combinedDistance = distance + calculateProximityDistance(sourceLine, patchLine)
+
+ if (combinedDistance < bestCombinedDistance) {
+ bestMatch = patchLine
+ bestDistance = distance
+ bestCombinedDistance = combinedDistance
+ }
+ }
+ }
+
+ if (bestMatch != null) {
+ // Establish the best match
+ sourceLine.matchingLine = bestMatch
+ bestMatch.matchingLine = sourceLine
+ }
+ }
+ }
+
+ private fun calculateProximityDistance(sourceLine: LineRecord, patchLine: LineRecord): Int {
+ // Implement logic to calculate proximity distance based on the distance to the nearest established link
+ // This is a simplified example. You may need a more sophisticated approach based on your specific requirements.
+ var distance = 0
+ if (sourceLine.previousLine?.matchingLine != null || patchLine.previousLine?.matchingLine != null) {
+ distance += 1
+ }
+ if (sourceLine.nextLine?.matchingLine != null || patchLine.nextLine?.matchingLine != null) {
+ distance += 1
+ }
+ return distance
+ }
+
+ private fun parseLines(text: String): List {
+ return text.lines().mapIndexed { index, line ->
+ LineRecord(index, line)
+ }
+ }
+
+ private fun parsePatchLines(text: String): List {
+ return text.lines().mapIndexed { index, line ->
+ LineRecord(
+ index = index, line = line.let {
+ when {
+ it.trimStart().startsWith("+") -> it.trimStart().substring(1)
+ it.trimStart().startsWith("-") -> it.trimStart().substring(1)
+ else -> it
+ }
+ }, type = when {
+ line.startsWith("+") -> LineType.ADD
+ line.startsWith("-") -> LineType.DELETE
+ else -> LineType.CONTEXT
+ }
+ )
+ }
+ }
+
+ private val log = LoggerFactory.getLogger(ApxPatchUtil::class.java)
+}
+
diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt
new file mode 100644
index 00000000..07204caf
--- /dev/null
+++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/PatchUtil.kt
@@ -0,0 +1,3 @@
+package com.github.simiacryptus.aicoder.util
+
+typealias PatchUtil = IterativePatchUtil
\ No newline at end of file
diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/WebDevApp.kt
index abb4710e..4e7c80a5 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
@@ -1,7 +1,7 @@
package com.simiacryptus.skyenet.apps.general
import com.github.simiacryptus.aicoder.actions.generic.commonRoot
-import com.github.simiacryptus.aicoder.util.addApplyDiffLinks2
+import com.github.simiacryptus.aicoder.util.addApplyFileDiffLinks
import com.simiacryptus.jopenai.API
import com.simiacryptus.jopenai.ApiModel
import com.simiacryptus.jopenai.ApiModel.Role
@@ -13,7 +13,6 @@ import com.simiacryptus.jopenai.util.JsonUtil
import com.simiacryptus.skyenet.Acceptable
import com.simiacryptus.skyenet.AgentPatterns
import com.simiacryptus.skyenet.core.actors.*
-import com.simiacryptus.skyenet.core.actors.CodingActor.Companion.indent
import com.simiacryptus.skyenet.core.platform.ClientManager
import com.simiacryptus.skyenet.core.platform.Session
import com.simiacryptus.skyenet.core.platform.StorageInterface
@@ -249,7 +248,7 @@ class WebDevAgent(
fun outputFn(task: SessionTask, design: String): StringBuilder? {
//val task = ui.newTask()
return task.complete(
- ui.socketManager.addApplyDiffLinks2(
+ ui.socketManager.addApplyFileDiffLinks(
root = codeFiles.keys.map { File(it).toPath() }.toTypedArray().commonRoot(),
code = codeFiles,
response = design,
diff --git a/webui/src/main/resources/application/main.js b/webui/src/main/resources/application/main.js
index d958a00a..5243df40 100644
--- a/webui/src/main/resources/application/main.js
+++ b/webui/src/main/resources/application/main.js
@@ -96,7 +96,7 @@ let loadImages = "true";
if (this.isPanning === false) return;
const dx = event.clientX - this.startX;
const dy = event.clientY - this.startY;
- if(this.priorPan) {
+ if (this.priorPan) {
if (this.currentTransform.x) {
this.currentTransform.x = dx * moveScale + this.priorPan.x;
} else {
@@ -146,7 +146,7 @@ function applyToAllSvg() {
function substituteMessages(outerMessageId, messageDiv) {
Object.entries(messageMap).forEach(([innerMessageId, content]) => {
- if(outerMessageId !== innerMessageId && messageDiv) messageDiv.querySelectorAll('[id="' + innerMessageId + '"]').forEach((element) => {
+ if (outerMessageId !== innerMessageId && messageDiv) messageDiv.querySelectorAll('[id="' + innerMessageId + '"]').forEach((element) => {
if (element.innerHTML !== content) {
//console.log("Substituting message with id " + innerMessageId + " and content " + content);
element.innerHTML = content;
@@ -169,8 +169,8 @@ function onWebSocketText(event) {
// console.log("Ignoring message with id " + messageId + " and version " + messageVersion);
// return;
// } else {
- messageVersions[messageId] = messageVersion;
- messageMap[messageId] = messageContent;
+ messageVersions[messageId] = messageVersion;
+ messageMap[messageId] = messageContent;
// }
// Cleanup: remove temporary event listeners
@@ -181,7 +181,7 @@ function onWebSocketText(event) {
substituteMessages(messageId, messageDiv);
}
});
- if(messageDivs.length == 0 && !messageId.startsWith("z")) {
+ if (messageDivs.length == 0 && !messageId.startsWith("z")) {
messageDiv = document.createElement('div');
messageDiv.className = 'message message-container'; // Add the message-container class
messageDiv.id = messageId;
@@ -209,7 +209,7 @@ function onWebSocketText(event) {
}
}
if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight;
- try{
+ try {
if (typeof Prism !== 'undefined') Prism.highlightAll();
} catch (e) {
console.log("Error highlighting code: " + e);
@@ -247,6 +247,8 @@ function updateTabs() {
event.stopPropagation();
const forTab = button.getAttribute('data-for-tab');
let tabsParent = button.closest('.tabs-container');
+ tabsParent.querySelectorAll('.tab-button').forEach(tabButton => tabButton.classList.remove('active'));
+ button.classList.add('active');
tabsParent.querySelectorAll('.tab-content').forEach(content => {
const contentParent = content.closest('.tabs-container');
if (contentParent === tabsParent) {
diff --git a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt
new file mode 100644
index 00000000..971cd095
--- /dev/null
+++ b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt
@@ -0,0 +1,38 @@
+package com.github.simiacryptus.aicoder.util
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+internal class ApxPatchUtilTest {
+
+ @Test
+ fun `test patch with simple addition`() {
+ val source = "Line 1\nLine 2\nLine 3"
+ val patch = """
+ +++
+ @@ -1,3 +1,4 @@
+ +Added Line
+ Line 1
+ Line 2
+ Line 3
+ """.trimIndent()
+ val expected = "Added Line\nLine 1\nLine 2\nLine 3"
+ val result = ApxPatchUtil.patch(source, patch)
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `test patch with deletion`() {
+ val source = "Line 1\nLine 2\nLine 3"
+ val patch = """
+ ---
+ @@ -1,3 +1,2 @@
+ -Line 2
+ Line 1
+ Line 3
+ """.trimIndent()
+ val expected = "Line 1\nLine 3"
+ val result = ApxPatchUtil.patch(source, patch)
+ assertEquals(expected, result)
+ }
+}
\ No newline at end of file
diff --git a/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt
new file mode 100644
index 00000000..f4cc732b
--- /dev/null
+++ b/webui/src/test/kotlin/com/github/simiacryptus/aicoder/util/IterativePatchUtilTest.kt
@@ -0,0 +1,152 @@
+package com.github.simiacryptus.aicoder.util
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class IterativePatchUtilTest {
+
+ @Test
+ fun testPatchExactMatch() {
+ val source = """
+ line1
+ line2
+ line3
+ """.trimIndent()
+ val patch = """
+ line1
+ line2
+ line3
+ """.trimIndent()
+ val result = IterativePatchUtil.patch(source, patch)
+ assertEquals(source.replace("\r\n", "\n"), result.replace("\r\n", "\n"))
+ }
+
+ @Test
+ fun testPatchAddLine() {
+ val source = """
+ line1
+ line2
+ line3
+ """.trimIndent()
+ val patch = """
+ line1
+ line2
+ +newLine
+ line3
+ """.trimIndent()
+ val expected = """
+ line1
+ line2
+ newLine
+ line3
+ """.trimIndent()
+ val result = IterativePatchUtil.patch(source, patch)
+ assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n"))
+ }
+
+ @Test
+ fun testPatchModifyLine() {
+ val source = """
+ line1
+ line2
+ line3
+ """.trimIndent()
+ val patch = """
+ line1
+ -line2
+ +modifiedLine2
+ line3
+ """.trimIndent()
+ val expected = """
+ line1
+ modifiedLine2
+ line3
+ """.trimIndent()
+ val result = IterativePatchUtil.patch(source, patch)
+ assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n"))
+ }
+
+ @Test
+ fun testPatchRemoveLine() {
+ val source = """
+ line1
+ line2
+ line3
+ """.trimIndent()
+ val patch = """
+ line1
+ line3
+ """.trimIndent()
+ val expected = """
+ line1
+ line3
+ """.trimIndent()
+ val result = IterativePatchUtil.patch(source, patch)
+ assertEquals(expected.replace("\r\n", "\n"), result.replace("\r\n", "\n"))
+ }
+ @Test
+ fun testFromData() {
+ val source = """
+ function updateTabs() {
+ document.querySelectorAll('.tab-button').forEach(button => {
+ button.addEventListener('click', (event) => { // Ensure the event is passed as a parameter
+ event.stopPropagation();
+ const forTab = button.getAttribute('data-for-tab');
+ let tabsParent = button.closest('.tabs-container');
+ tabsParent.querySelectorAll('.tab-content').forEach(content => {
+ const contentParent = content.closest('.tabs-container');
+ if (contentParent === tabsParent) {
+ if (content.getAttribute('data-tab') === forTab) {
+ content.classList.add('active');
+ } else if (content.classList.contains('active')) {
+ content.classList.remove('active')
+ }
+ }
+ });
+ })
+ });
+ }
+ """.trimIndent()
+ val patch = """
+ tabsParent.querySelectorAll('.tab-content').forEach(content => {
+ const contentParent = content.closest('.tabs-container');
+ if (contentParent === tabsParent) {
+ if (content.getAttribute('data-tab') === forTab) {
+ content.classList.add('active');
+ + button.classList.add('active'); // Mark the button as active
+ } else if (content.classList.contains('active')) {
+ content.classList.remove('active')
+ + button.classList.remove('active'); // Ensure the button is not marked as active
+ }
+ }
+ });
+ """.trimIndent()
+ val expected = """
+ function updateTabs() {
+ document.querySelectorAll('.tab-button').forEach(button => {
+ button.addEventListener('click', (event) => { // Ensure the event is passed as a parameter
+ event.stopPropagation();
+ const forTab = button.getAttribute('data-for-tab');
+ let tabsParent = button.closest('.tabs-container');
+ tabsParent.querySelectorAll('.tab-content').forEach(content => {
+ const contentParent = content.closest('.tabs-container');
+ if (contentParent === tabsParent) {
+ if (content.getAttribute('data-tab') === forTab) {
+ content.classList.add('active');
+ button.classList.add('active'); // Mark the button as active
+ } else if (content.classList.contains('active')) {
+ content.classList.remove('active')
+ button.classList.remove('active'); // Ensure the button is not marked as active
+ }
+ }
+ });
+ })
+ });
+ }
+ """.trimIndent()
+ val result = IterativePatchUtil.patch(source, patch)
+ assertEquals(
+ expected.replace("\r\n", "\n").replace("\\s{1,}".toRegex(), " "),
+ result.replace("\r\n", "\n").replace("\\s{1,}".toRegex(), " "))
+ }
+}
\ No newline at end of file