From d4218adc622e421b0557dfbbd0f640d4fd0e7eef Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Fri, 29 Mar 2024 00:00:17 -0400 Subject: [PATCH] 1.0.58 (#63) * 1.0.58 * 1.0.58 * Update Acceptable.kt * Update CodingAgent.kt --- .../skyenet/core/actors/CodingActor.kt | 11 +- .../core/util/ClasspathRelationships.kt | 414 ++++++++++++++++++ gradle.properties | 2 +- .../aicoder/util/SimpleDiffUtil.kt | 48 +- .../com/simiacryptus/skyenet/Acceptable.kt | 177 +++++--- .../com/simiacryptus/skyenet/AgentPatterns.kt | 63 --- .../com/simiacryptus/skyenet/Retryable.kt | 57 +-- .../com/simiacryptus/skyenet/TabbedDisplay.kt | 8 +- .../skyenet/apps/coding/CodingAgent.kt | 70 +-- .../skyenet/apps/coding/CodingSubAgent.kt | 69 +++ .../skyenet/apps/coding/ShellToolAgent.kt | 42 +- .../skyenet/apps/coding/ToolAgent.kt | 6 +- .../skyenet/apps/general/WebDevApp.kt | 4 +- .../skyenet/webui/chat/ChatSocketManager.kt | 2 +- .../skyenet/webui/session/SessionTask.kt | 6 +- .../webui/session/SocketManagerBase.kt | 4 + .../skyenet/webui/test/CodingActorTestApp.kt | 3 +- .../simiacryptus/aicoder/util/DiffUtilTest.kt | 1 + 18 files changed, 723 insertions(+), 264 deletions(-) create mode 100644 webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingSubAgent.kt diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt index 0fefca7c..f690a2ed 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/actors/CodingActor.kt @@ -9,7 +9,6 @@ import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.skyenet.core.OutputInterceptor import com.simiacryptus.skyenet.interpreter.Interpreter -import org.apache.commons.text.StringEscapeUtils.escapeHtml4 import java.util.* import javax.script.ScriptException import kotlin.reflect.KClass @@ -311,7 +310,7 @@ open class CodingActor( Role.assistant, """ |```${language.lowercase()} - |${previousCode?.let { escapeHtml4(it).indent(" ") }} + |${previousCode?.let { /*escapeHtml4*/(it).indent(" ") }} |``` |""".trimMargin().trim().toContentList() ), @@ -321,7 +320,7 @@ open class CodingActor( |The previous code failed with the following error: | |``` - |${error.message?.trim() ?: ""?.let { escapeHtml4(it).indent(" ") }} + |${error.message?.trim() ?: ""?.let { /*escapeHtml4*/(it).indent(" ") }} |``` | |Correct the code and try again. @@ -393,9 +392,9 @@ open class CodingActor( fun getRenderedResponse(respondWithCode: List>, defaultLanguage: String = "") = respondWithCode.joinToString("\n") { when (it.first) { - "code" -> "```$defaultLanguage\n${it.second?.let { escapeHtml4(it).indent(" ") }}\n```" - "text" -> it.second?.let { escapeHtml4(it).indent(" ") }.toString() - else -> "```${it.first}\n${it.second?.let { escapeHtml4(it).indent(" ") }}\n```" + "code" -> "```$defaultLanguage\n${it.second?.let { /*escapeHtml4*/(it).indent(" ") }}\n```" + "text" -> it.second?.let { /*escapeHtml4*/(it).indent(" ") }.toString() + else -> "```${it.first}\n${it.second?.let { /*escapeHtml4*/(it).indent(" ") }}\n```" } } diff --git a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/ClasspathRelationships.kt b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/ClasspathRelationships.kt index b7467417..0feec9cb 100644 --- a/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/ClasspathRelationships.kt +++ b/core/src/main/kotlin/com/simiacryptus/skyenet/core/util/ClasspathRelationships.kt @@ -1,5 +1,9 @@ package com.simiacryptus.skyenet.core.util +import org.objectweb.asm.* +import org.objectweb.asm.signature.SignatureReader +import org.objectweb.asm.signature.SignatureVisitor +import java.io.File import java.util.jar.JarFile object ClasspathRelationships { @@ -7,6 +11,345 @@ object ClasspathRelationships { open val from_method : String = "" open val to_method : String = "" } + data object INHERITANCE : Relation() // When a class extends another class + data object INTERFACE_IMPLEMENTATION : Relation() // When a class implements an interface + data class FIELD_TYPE(override val from_method: String) : Relation() // When a class has a field of another class type + data class METHOD_PARAMETER( + override val from_method: String + ) : Relation() // When a class has a method that takes another class as a parameter + + data object METHOD_RETURN_TYPE : Relation() // When a class has a method that returns another class + data class LOCAL_VARIABLE(override val from_method: String) : + Relation() // When a method within a class declares a local variable of another class + + data class EXCEPTION_TYPE(override val from_method: String) : + Relation() // When a method declares that it throws an exception of another class + + data class ANNOTATION(override val from_method: String) : + Relation() // When a class, method, or field is annotated with another class (annotation) + + data class INSTANCE_CREATION(override val from_method: String) : Relation() // When a class creates an instance of another class + data class METHOD_REFERENCE( + override val from_method: String, + override val to_method: String + ) : Relation() // When a method references another class's method + data class METHOD_SIGNATURE( + override val from_method: String, + override val to_method: String + ) : Relation() // When a method signature references another class + + data class FIELD_REFERENCE(override val from_method: String) : Relation() // When a method references another class's field + data class DYNAMIC_BINDING(override val from_method: String) : + Relation() // When a class uses dynamic binding (e.g., invoke dynamic) related to another class + + data object OUTER_CLASS : Relation() // When a class references its outer class + + data object UNKNOWN : Relation() // A fallback for unknown or unclassified dependencies + + + class DependencyClassVisitor( + val dependencies: MutableMap> = mutableMapOf(), + var access: Int = 0, + var methods: MutableMap = mutableMapOf(), + ) : ClassVisitor(Opcodes.ASM9) { + + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array? + ) { + this.access = access + // Add superclass dependency + superName?.let { addDep(it, INHERITANCE) } + // Add interface dependencies + interfaces?.forEach { addDep(it, INTERFACE_IMPLEMENTATION) } + visitSignature(name, signature) + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitField( + access: Int, + name: String?, + desc: String?, + signature: String?, + value: Any? + ): FieldVisitor? { + visitSignature(name, signature) + // Add field type dependency + addType(desc, FIELD_TYPE(from_method = "")) + return DependencyFieldVisitor(dependencies) + } + + override fun visitMethod( + access: Int, + name: String?, + desc: String?, + signature: String?, + exceptions: Array? + ): MethodVisitor { + visitSignature(name, signature) + // Add method return type and parameter types dependencies + addMethodDescriptor(desc, METHOD_PARAMETER(from_method = name ?: ""), METHOD_RETURN_TYPE) + // Add exception types dependencies + exceptions?.forEach { addDep(it, EXCEPTION_TYPE(from_method = name ?: "")) } + val methodVisitor = DependencyMethodVisitor(name ?: "", dependencies) + methods[methodVisitor.name] = methodVisitor + return methodVisitor + } + + private fun visitSignature(name: String?, signature: String?) { + // Check if the name indicates an inner class or property accessor + if (name?.contains("$") == true) { + // NOTE: This isn't a typically required dependency + // addDep(name.substringBefore("$"), OUTER_CLASS) + } + if (name?.contains("baseClassLoader") == true) { + signature?.let { + val signatureReader = SignatureReader(it) + signatureReader.accept(object : SignatureVisitor(Opcodes.ASM9) { + override fun visitClassType(name: String?) { + name?.let { addDep(it, METHOD_PARAMETER(from_method = "")) } + } + }) + } + return + } + signature?.let { + val signatureReader = SignatureReader(it) + signatureReader.accept(object : SignatureVisitor(Opcodes.ASM9) { + override fun visitClassType(name: String?) { + name?.let { addDep(it, METHOD_PARAMETER(from_method = "")) } + } + }) + } + } + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + // Add annotation type dependency + addType(descriptor, ANNOTATION(from_method = "")) + return super.visitAnnotation(descriptor, visible) + } + + private fun addDep(internalName: String, relationType: Relation) { + val typeName = internalName.replace('/', '.') + dependencies.getOrPut(typeName) { mutableSetOf() }.add(relationType) + } + + private fun addType(type: String?, relationType: Relation) { + type?.let { + val typeName = Type.getType(it).className + addDep(typeName, relationType) + } + } + + private fun addMethodDescriptor( + descriptor: String?, + paramRelationType: Relation, + returnRelationType: Relation + ) { + descriptor?.let { + val methodType = Type.getMethodType(it) + // Add return type dependency + addType(methodType.returnType.descriptor, returnRelationType) + // Add parameter types dependencies + methodType.argumentTypes.forEach { argType -> + addType(argType.descriptor, paramRelationType) + } + } + } + + } + + class DependencyFieldVisitor( + val dependencies: MutableMap> + ) : FieldVisitor(Opcodes.ASM9) { + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + descriptor?.let { addType(it, ANNOTATION(from_method = "")) } + return super.visitAnnotation(descriptor, visible) + } + + override fun visitAttribute(attribute: Attribute?) { + super.visitAttribute(attribute) + } + + override fun visitTypeAnnotation( + typeRef: Int, + typePath: TypePath?, + descriptor: String?, + visible: Boolean + ): AnnotationVisitor? { + descriptor?.let { addType(it, ANNOTATION(from_method = "")) } + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible) + } + + private fun addDep(internalName: String, relationType: Relation) { + val typeName = internalName.replace('/', '.') + dependencies.getOrPut(typeName) { mutableSetOf() }.add(relationType) + } + + private fun addType(type: String, relationType: Relation) { + addDep(getTypeName(type) ?: return, relationType) + } + + } + + class DependencyMethodVisitor( + val name: String, + val dependencies: MutableMap>, + var access: Int = 0, + ) : MethodVisitor(Opcodes.ASM9) { + + + override fun visitMethodInsn( + opcode: Int, + owner: String?, + name: String?, + descriptor: String?, + isInterface: Boolean + ) { + access = opcode + // Add method reference dependency + owner?.let { addDep(it, METHOD_REFERENCE(from_method = this.name, to_method = name ?: "")) } + // Add method descriptor dependencies (for parameter and return types) + descriptor?.let { addMethodDescriptor(it, METHOD_SIGNATURE(from_method = this.name, to_method = name ?: "")) } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + } + + override fun visitParameter(name: String?, access: Int) { + // Add method parameter type dependency + name?.let { addType(it, METHOD_PARAMETER(from_method = this.name)) } + super.visitParameter(name, access) + } + + + override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, descriptor: String?) { + // Add field reference dependency + owner?.let { addDep(it, FIELD_REFERENCE(from_method = this.name)) } + // Add field type dependency + descriptor?.let { addType(it, FIELD_TYPE(from_method = this.name)) } + super.visitFieldInsn(opcode, owner, name, descriptor) + } + + override fun visitTypeInsn(opcode: Int, type: String?) { + // Add instance creation or local variable dependency based on opcode + type?.let { + val dependencyType = when (opcode) { + Opcodes.NEW -> INSTANCE_CREATION(from_method = this.name) + else -> LOCAL_VARIABLE(from_method = this.name) + } + addType(it, dependencyType) + } + super.visitTypeInsn(opcode, type) + } + + override fun visitLdcInsn(value: Any?) { + // Add class literal dependency + if (value is Type) { + addType(value.descriptor, LOCAL_VARIABLE(from_method = this.name)) + } + super.visitLdcInsn(value) + } + + override fun visitMultiANewArrayInsn(descriptor: String?, numDimensions: Int) { + // Add local variable dependency for multi-dimensional arrays + descriptor?.let { addType(it, LOCAL_VARIABLE(from_method = this.name)) } + super.visitMultiANewArrayInsn(descriptor, numDimensions) + } + + override fun visitInvokeDynamicInsn( + name: String?, + descriptor: String?, + bootstrapMethodHandle: Handle?, + vararg bootstrapMethodArguments: Any? + ) { + // Add dynamic binding dependency + descriptor?.let { addMethodDescriptor(it, DYNAMIC_BINDING(from_method = this.name)) } + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) + } + + override fun visitLocalVariable( + name: String?, + descriptor: String?, + signature: String?, + start: Label?, + end: Label?, + index: Int + ) { + // Add local variable dependency + descriptor?.let { addType(it, LOCAL_VARIABLE(from_method = this.name)) } + super.visitLocalVariable(name, descriptor, signature, start, end, index) + } + + override fun visitTryCatchBlock(start: Label?, end: Label?, handler: Label?, type: String?) { + // Add exception type dependency + type?.let { addType(it, EXCEPTION_TYPE(from_method = this.name)) } + super.visitTryCatchBlock(start, end, handler, type) + } + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + // Add annotation type dependency + descriptor?.let { addType(it, ANNOTATION(from_method = this.name)) } + return super.visitAnnotation(descriptor, visible) + } + + private fun addDep(internalName: String, relationType: Relation) { + val typeName = internalName.replace('/', '.') + dependencies.getOrPut(typeName) { mutableSetOf() }.add(relationType) + } + + private fun addType(type: String, relationType: Relation): Unit { + addDep(getTypeName(type) ?: return, relationType) + } + + private fun addMethodDescriptor( + descriptor: String, + relationType: Relation + ) { + val methodType = Type.getMethodType(descriptor) + // Add return type dependency + addType(methodType.returnType.descriptor, relationType) + // Add parameter types dependencies + methodType.argumentTypes.forEach { addType(it.descriptor, relationType) } + } + } + + private fun getTypeName(type: String): String? = try { + val name = when { + // For array types, get the class name + type.startsWith("L") && type.endsWith(";") -> getTypeName(type.substring(1, type.length - 1)) + // Handle the case where the descriptor appears to be a plain class name + !type.startsWith("[") && !type.startsWith("L") && !type.endsWith(";") -> Type.getObjectType(type.classToPath).className + // Handle the case where the descriptor is missing 'L' and ';' + type.contains("/") && !type.startsWith("L") && !type.endsWith(";") -> Type.getObjectType(type).className + // For primitive types, use the descriptor directly + type.length == 1 && "BCDFIJSZ".contains(type[0]) -> type + type.endsWith("$") -> type.substring(0, type.length - 1) + else -> Type.getType(type).className + } + name + } catch (e: Exception) { + println("Error adding type: $type (${e.message})") + null + } + + + val String.classToPath + get() = removeSuffix(".class").replace('.', '/') + + + val String.jarFiles + get() = File(this).listFiles()?.filter { + it.isFile && it.name.endsWith(".jar") + } + val CharSequence.symbolName + get() = + replace("""^[\[ILZBCFDJSV]+""".toRegex(), "") + .removeSuffix(";").replace('/', '.') + .removeSuffix(".class") data class Reference( val from: String, @@ -14,6 +357,30 @@ object ClasspathRelationships { val relation: Relation ) + fun analyzeJar(jarPath: String) = analyzeJar(readJarClasses(jarPath)) + fun analyzeJar( + jar: Map, + ) = jar.flatMap { (className, classData) -> + val dependencyClassVisitor = DependencyClassVisitor() + ClassReader(classData).accept(dependencyClassVisitor, 0) + val dependencies = dependencyClassVisitor.dependencies + dependencies.flatMap { (to, dependencies) -> + dependencies.map { Reference(className, to, it) } + } + } + + fun classAccessMap(jarPath: String) = classAccessMap(readJarClasses(jarPath)) + + fun classAccessMap( + jar: Map, + ): Map = jar.flatMap { (className, classData) -> + val dependencyClassVisitor = DependencyClassVisitor() + ClassReader(classData).accept(dependencyClassVisitor, 0) + val methodData = + dependencyClassVisitor.methods.mapValues { it.value.access }.entries.map { it.key to it.value }.toMap() + listOf(className to dependencyClassVisitor.access) + methodData.map { className + "::" + it.key to it.value } + }.toMap() + fun readJarClasses(jarPath: String) = JarFile(jarPath).use { jarFile -> jarFile.entries().asSequence().filter { it.name.endsWith(".class") }.map { entry -> @@ -26,6 +393,53 @@ object ClasspathRelationships { jarFile.entries().asSequence().map { it.name }.toList().toTypedArray() } + fun upstream( + dependencies: List, + className: String, + buffer: MutableSet = mutableSetOf(className) + ) = upstream(upstreamMap(dependencies), className, buffer) + + fun upstreamMap(dependencies: List) = + dependencies.groupBy { it.to } + + fun upstream( + dependencies: Map>, + className: String, + buffer: MutableSet = mutableSetOf(className) + ): Set { + val required = (dependencies[className] ?: listOf()) + .map { it.from } + .filter { className != it } + .filter { !buffer.contains(it) } + .filter { it.isNotBlank() } + .toTypedArray() + synchronized(buffer) { buffer.addAll(required) } + required.toList().parallelStream().forEach { upstream(dependencies, it, buffer).stream() } + return buffer + } + + fun downstream( + dependencies: Map>, + className: String, + buffer: MutableSet = mutableSetOf(className) + ): Set { + val required = (dependencies[className] ?: listOf()) + .map { it.to } + .filter { className != it } + .filter { !buffer.contains(it) } + .filter { it.isNotBlank() } + .toTypedArray() + synchronized(buffer) { buffer.addAll(required) } + required.toList().parallelStream().forEach { downstream(dependencies, it, buffer).stream() } + return buffer + } + + fun downstream( + dependencies: List, + className: String, + buffer: MutableSet = mutableSetOf(className) + ) = downstream(downstreamMap(dependencies), className, buffer) + fun downstreamMap(dependencies: List) = dependencies.groupBy { it.from } diff --git a/gradle.properties b/gradle.properties index 38e2d90b..3c1ad70f 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.57 +libraryVersion = 1.0.58 gradleVersion = 7.6.1 diff --git a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt index 19c324c3..4945fb48 100644 --- a/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt +++ b/webui/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt @@ -6,7 +6,6 @@ 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.StringEscapeUtils.escapeHtml4 import org.apache.commons.text.similarity.LevenshteinDistance import org.slf4j.LoggerFactory import java.io.File @@ -199,7 +198,7 @@ fun SocketManagerBase.addSaveLinks( task.error(null, e) } } - markdown.replace(codeValue + "```", codeValue?.let { escapeHtml4(it).indent(" ") } + "```\n" + hrefLink) + markdown.replace(codeValue + "```", codeValue?.let { /*escapeHtml4*/(it).indent(" ") } + "```\n" + hrefLink) } return withLinks } @@ -242,12 +241,24 @@ fun SocketManagerBase.addApplyDiffLinks2( val matches = diffPattern.findAll(response).toList() val withLinks = matches.fold(response) { markdown, diffBlock -> val filename = diffBlock.groupValues[1] - val filepath = findFile(root, filename) ?: root.resolve(filename) + val filepath = try { + findFile(root, filename) ?: root.resolve(filename) + } catch (e: Throwable) { + log.error("Error finding file: $filename", e) + root.resolve(filename) + } val diffVal = diffBlock.groupValues[2] val prevCode = try { if (!filepath.toFile().exists()) { - log.warn("File not found: $filepath") + log.warn( + """ + |File not found: $filepath + |Root: ${root.toAbsolutePath()} + |Files: + |${code.keys.joinToString("\n") { "* $it" }} + """.trimMargin() + ) "" } else { filepath.readText(Charsets.UTF_8) @@ -258,12 +269,23 @@ fun SocketManagerBase.addApplyDiffLinks2( } val newCode = SimpleDiffUtil.patch(prevCode, diffVal) val echoDiff = try { - DiffUtil.formatDiff(DiffUtil.generateDiff(prevCode.lines(), newCode.lines())) - } catch (e: Throwable) { renderMarkdown("```\n${e.stackTraceToString()}\n```") } + DiffUtil.formatDiff( + DiffUtil.generateDiff( + prevCode.lines(), + newCode.lines() + ) + ) + } catch (e: Throwable) { + renderMarkdown("```\n${e.stackTraceToString()}\n```") + } val hrefLink = hrefLink("Apply Diff") { try { - handle(mapOf(root.relativize(filepath).toString() to SimpleDiffUtil.patch(prevCode, diffVal))) + handle(mapOf(root.relativize(filepath).toString() to SimpleDiffUtil.patch( + prevCode, + diffVal + ) + )) task.complete("""
Diff Applied
""") } catch (e: Throwable) { task.error(null, e) @@ -300,7 +322,17 @@ fun SocketManagerBase.addApplyDiffLinks2( diffTask?.add(renderMarkdown(/*escapeHtml4*/(diffVal))) newCodeTask?.add(renderMarkdown("# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${newCode}\n```")) prevCodeTask?.add(renderMarkdown("# $filename\n\n```${filename.split('.').lastOrNull() ?: ""}\n${prevCode}\n```")) - patchTask?.add(renderMarkdown("# $filename\n\n```diff\n ${echoDiff?.let { /*escapeHtml4*/URLDecoder.decode(it, Charsets.UTF_8)/*.indent(" ")*/ }}\n```")) + patchTask?.add( + renderMarkdown( + "# $filename\n\n```diff\n ${ + echoDiff?.let { /*escapeHtml4*/URLDecoder.decode( + it, + Charsets.UTF_8 + )/*.indent(" ")*/ + } + }\n```" + ) + ) }, 100, TimeUnit.MILLISECONDS) markdown.replace( diffVal, inTabs + "\n" + hrefLink + "\n" + reverseHrefLink diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt index e6f638a1..6b55799b 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Acceptable.kt @@ -36,7 +36,7 @@ class Acceptable( val idx: Int = size set(label(idx), "Retrying...") task.add("") - main(idx) + main(idx, this@Acceptable.task) } } @@ -45,91 +45,138 @@ class Acceptable( } private val acceptGuard = AtomicBoolean(false) - fun main(tabIndex: Int = tabs.size) { + fun main(tabIndex: Int = tabs.size, task: SessionTask = this.task) { try { val history = mutableListOf>() history.add(userMessage to Role.user) - var design = initialResponse(userMessage) + val design = initialResponse(userMessage) history.add(outputFn(design) to Role.assistant) - var acceptLink: String? = null - var textInput: String? = null - val feedbackGuard = AtomicBoolean(false) - fun feedbackForm() = """ - |
- |${acceptLink!!} - |
- |${textInput!!} - """.trimMargin() - val tabLabel = tabs.label(tabIndex) val tabContent = tabs[tabLabel] ?: tabs.set(tabLabel, "") - textInput = ui.textInput { userResponse -> - if (feedbackGuard.getAndSet(true)) return@textInput - try { - val prevValue = tabContent.toString() - val newValue = (prevValue.substringBefore("") - + "" - + prevValue.substringAfter("") - + renderMarkdown(userResponse)) - tabContent.set(newValue) - history.add(renderMarkdown(userResponse) to Role.user) - task.add("") // Show spinner - tabs.update() - design = reviseResponse(history + listOf(userResponse to Role.user)) - task.complete() - tabContent.set(renderMarkdown(newValue) + "\n" + outputFn(design) + "\n" + feedbackForm()) - tabs.update() - } catch (e: Exception) { - task.error(ui, e) - throw e - } finally { - feedbackGuard.set(false) - } - } - acceptLink = "" + - ui.hrefLink("\uD83D\uDC4D") { - if (acceptGuard.getAndSet(true)) { - return@hrefLink - } - try { - tabs.selectedTab = tabIndex - tabContent?.apply { - val prevTab = toString() - val newValue = - prevTab.substringBefore("") + "" + prevTab.substringAfter( - "" - ) - set(newValue) - tabs.update() - } ?: throw IllegalStateException("Tab $tabIndex not found") - } catch (e: Exception) { - task.error(ui, e) - acceptGuard.set(false) - throw e - } - atomicRef.set(design) - semaphore.release() - } + "" + if (tabs.size > tabIndex) { - tabContent?.append(outputFn(design) + "\n" + feedbackForm()) + tabContent.append(outputFn(design) + "\n" + feedbackForm(tabIndex, tabContent, design, history, task)) } else { - tabContent?.set(outputFn(design) + "\n" + feedbackForm()) + tabContent.set(outputFn(design) + "\n" + feedbackForm(tabIndex, tabContent, design, history, task)) } tabs.update() } catch (e: Throwable) { task.error(ui, e) task.complete(ui.hrefLink("🔄 Retry") { - main() + main(task = task) }) } } + private fun feedbackForm( + tabIndex: Int?, + tabContent: StringBuilder, + design: T, + history: List>, + task: SessionTask, + ): String = """ + | + |
+ |${acceptLink(tabIndex, tabContent, design)!!} + |
+ |${textInput(design, tabContent, history, task)!!} + | + """.trimMargin() + + private fun acceptLink( + tabIndex: Int?, + tabContent: StringBuilder, + design: T, + ) = ui.hrefLink("\uD83D\uDC4D") { + accept(tabIndex, tabContent, design) + } + + private fun textInput( + design: T, + tabContent: StringBuilder, + history: List>, + task: SessionTask, + ): String { + val feedbackGuard = AtomicBoolean(false) + return ui.textInput { userResponse -> + if (feedbackGuard.getAndSet(true)) return@textInput + try { + feedback(tabContent, userResponse, history, design, task) + } catch (e: Exception) { + task.error(ui, e) + throw e + } finally { + feedbackGuard.set(false) + } + } + } + + private fun feedback( + tabContent: StringBuilder, + userResponse: String, + history: List>, + design: T, + task: SessionTask, + ) { + var history = history + history = history + (userResponse to Role.user) + val prevValue = tabContent.toString() + val newValue = (prevValue.substringBefore("") + + "" + + prevValue.substringAfter("") + + "
" + + renderMarkdown(userResponse) + + "
") + tabContent.set(newValue) + task.add("") // Show spinner + tabs.update() + val newDesign = reviseResponse(history) + val newTask = ui.newTask() + tabContent.set(newValue + "\n" + newTask.placeholder) + tabs.update() + task.complete() + Retryable(ui, newTask) { + outputFn(newDesign) + "\n" + feedbackForm( + tabIndex = null, + tabContent = it, + design = newDesign, + history = history, + task = newTask + ) + }.apply { + set(label(size), process(container)) + } + } + + private fun accept(tabIndex: Int?, tabContent: StringBuilder, design: T) { + if (acceptGuard.getAndSet(true)) { + return + } + try { + if(null != tabIndex) tabs.selectedTab = tabIndex + tabContent?.apply { + val prevTab = toString() + val newValue = + prevTab.substringBefore("") + "" + prevTab.substringAfter( + "" + ) + set(newValue) + tabs.update() + } + } catch (e: Exception) { + task.error(ui, e) + acceptGuard.set(false) + throw e + } + atomicRef.set(design) + semaphore.release() + } + override fun call(): T { task.echo(heading) main() semaphore.acquire() - val result = atomicRef.get() - return result + return atomicRef.get() } } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt index 184749ca..078a7146 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/AgentPatterns.kt @@ -1,20 +1,9 @@ package com.simiacryptus.skyenet -import com.simiacryptus.jopenai.API -import com.simiacryptus.jopenai.ApiModel -import com.simiacryptus.jopenai.ApiModel.Role -import com.simiacryptus.jopenai.util.ClientUtil.toContentList -import com.simiacryptus.skyenet.core.actors.BaseActor import com.simiacryptus.skyenet.core.actors.CodingActor.Companion.indent -import com.simiacryptus.skyenet.webui.application.ApplicationInterface -import com.simiacryptus.skyenet.webui.session.SessionTask -import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown -import java.util.concurrent.Semaphore -import java.util.concurrent.atomic.AtomicReference object AgentPatterns { - fun displayMapInTabs( map: Map, ) = """ @@ -43,56 +32,4 @@ object AgentPatterns { | """.trimMargin() - fun retryable( - ui: ApplicationInterface, - task: SessionTask, - process: (StringBuilder) -> String, - ) = Retryable(ui, task, process).apply { addTab(ui, process(container!!)) } - - fun iterate( - ui: ApplicationInterface, - userMessage: String, - heading: String = renderMarkdown(userMessage), - initialResponse: (String) -> T, - reviseResponse: (List>) -> T, - outputFn: (T) -> String = { design -> renderMarkdown(design.toString()) }, - task: SessionTask - ): T = Acceptable( - task = task, - userMessage = userMessage, - initialResponse = initialResponse, - outputFn = outputFn, - ui = ui, - reviseResponse = reviseResponse, - heading = heading - ).call() - - - fun iterate( - input: String, - heading: String = renderMarkdown(input), - actor: BaseActor, - toInput: (String) -> I, - api: API, - ui: ApplicationInterface, - outputFn: (T) -> String = { design -> renderMarkdown(design.toString()) }, - task: SessionTask - ) = Acceptable( - task = task, - userMessage = input, - initialResponse = { it: String -> actor.answer(toInput(it), api = api) }, - outputFn = outputFn, - ui = ui, - reviseResponse = { userMessages: List> -> - actor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) }.toTypedArray()), - input = toInput(input), - api = api - ) - }, - atomicRef = AtomicReference(), - semaphore = Semaphore(0), - heading = heading - ).call() - } \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt index 17c75868..f66ed436 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/Retryable.kt @@ -5,53 +5,30 @@ import com.simiacryptus.skyenet.webui.session.SessionTask open class Retryable( val ui: ApplicationInterface, - val task: SessionTask, - open val process: (StringBuilder) -> String, -) { - val container = task.add("
") - private val history = mutableListOf() - fun newHTML(ui: ApplicationInterface): String = """ -
-
- ${ - history.withIndex().joinToString("\n") { (index, _) -> + task: SessionTask, + val process: (StringBuilder) -> String +) : TabbedDisplay(task) { + + override fun renderTabButtons(): String = """ +
${ + tabs.withIndex().joinToString("\n") { (index, _) -> val tabId = "$index" """""" } } ${ ui.hrefLink("â™»") { - val idx = history.size - history.add("Retrying...") - container?.clear() - container?.append(newHTML(ui)) - task.add("") - val newResult = process(container!!) - addTab(ui, newResult, idx) + val idx = tabs.size + val label = label(idx) + val content = StringBuilder("Retrying..." + SessionTask.spinner) + tabs.add(label to content) + update() + val newResult = process(content) + content.clear() + set(label, newResult) } }
- ${ - history.withIndex().joinToString("\n") { (index, content) -> - """ -
- $content -
- """.trimIndent() - } - } -
-""".trimIndent() + """.trimIndent() - fun addTab(ui: ApplicationInterface, content: String, idx: Int = history.size): String { - if (idx < history.size) { - history.set(idx, content) - } else { - history.add(content) - } - container?.clear() - container?.append(newHTML(ui)) - task.complete() - return content - } -} \ No newline at end of file +} diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt index bc02d3f3..98a43148 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/TabbedDisplay.kt @@ -9,7 +9,6 @@ open class TabbedDisplay( var selectedTab: Int = 0 companion object { val log = org.slf4j.LoggerFactory.getLogger(TabbedDisplay::class.java) -// val scheduledPool = java.util.concurrent.Executors.newScheduledThreadPool(1) } val size: Int get() = tabs.size open fun render() = """ @@ -63,10 +62,7 @@ open class TabbedDisplay( fun find(name: String) = tabs.withIndex().firstOrNull { it.value.first == name }?.index open fun label(i: Int): String { - return when { - tabs.size <= i -> "${tabs.size + 1}" - else -> tabs[i].first - } + return "${tabs.size + 1}" } fun clear() { @@ -74,7 +70,7 @@ open class TabbedDisplay( update() } - fun update() { + open fun update() { if(container != null) synchronized(container) { container.clear() container.append(render()) diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt index 78485bee..948361bd 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingAgent.kt @@ -5,6 +5,7 @@ import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.proxy.ValidatedObject +import com.simiacryptus.skyenet.Retryable import com.simiacryptus.skyenet.core.actors.ActorSystem import com.simiacryptus.skyenet.core.actors.CodingActor import com.simiacryptus.skyenet.core.actors.CodingActor.CodeResult @@ -18,23 +19,24 @@ import com.simiacryptus.skyenet.interpreter.Interpreter import com.simiacryptus.skyenet.webui.application.ApplicationInterface import com.simiacryptus.skyenet.webui.session.SessionTask import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown -import org.apache.commons.text.StringEscapeUtils -import org.apache.commons.text.StringEscapeUtils.escapeHtml4 import org.slf4j.LoggerFactory import java.util.* +import java.util.concurrent.TimeUnit import kotlin.reflect.KClass + open class CodingAgent( val api: API, dataStorage: StorageInterface, session: Session, user: User?, val ui: ApplicationInterface, - val interpreter: KClass, + interpreter: KClass, val symbols: Map, temperature: Double = 0.1, val details: String? = null, val model: ChatModels, + private val mainTask: SessionTask = ui.newTask(), val actorMap: Map = mapOf( ActorTypes.CodingActor to CodingActor(interpreter, symbols = symbols, temperature = temperature, details = details, model = model) ), @@ -54,18 +56,38 @@ open class CodingAgent( OperationType.Execute ) } + val scheduledThreadPoolExecutor = java.util.concurrent.Executors.newScheduledThreadPool(1) + val cachedThreadPoolExecutor = java.util.concurrent.Executors.newCachedThreadPool() fun start( userMessage: String, ) { - val message = ui.newTask() try { - message.echo(renderMarkdown(userMessage)) + mainTask.echo(renderMarkdown(userMessage)) val codeRequest = codeRequest(listOf(userMessage to ApiModel.Role.user)) - displayCode(message, codeRequest) + newRetryable(mainTask, codeRequest) } catch (e: Throwable) { log.warn("Error", e) - message.error(ui, e) + mainTask.error(ui, e) + } + } + + private fun newRetryable(task: SessionTask, codeRequest: CodingActor.CodeRequest) { + val newTask = ui.newTask() + task.complete(newTask.placeholder) + Retryable(ui, newTask) { + val newTask = ui.newTask() + scheduledThreadPoolExecutor.schedule({ + cachedThreadPoolExecutor.submit { + val statusSB = newTask.add("Running...") + displayCode(newTask, codeRequest) + statusSB?.clear() + newTask.complete() + } + }, 100, TimeUnit.MILLISECONDS) + newTask.placeholder + }.apply { + set(label(size), process(container)) } } @@ -91,16 +113,6 @@ open class CodingAgent( displayCodeAndFeedback(task, codeRequest, codeResponse) } catch (e: Throwable) { log.warn("Error", e) - val error = task.error(ui, e) - var regenButton: StringBuilder? = null - regenButton = task.complete(ui.hrefLink("â™»", "href-link regen-button"){ - regenButton?.clear() - val header = task.header("Regenerating...") - displayCode(task, codeRequest) - header?.clear() - error?.clear() - task.complete() - }) } } protected fun displayCodeAndFeedback( @@ -150,8 +162,7 @@ open class CodingAgent( formHandle = task.add( """ |
- |${playButton(task, request, response, formText) { formHandle!! }} - |${regenButton(task, request, formText) { formHandle!! }} + |${if (!canPlay) "" else playButton(task, request, response, formText) { formHandle!! }} |
|${reviseMsg(task, request, response, formText) { formHandle!! }} """.trimMargin(), className = "reply-message" @@ -170,7 +181,7 @@ open class CodingAgent( formHandle: () -> StringBuilder ) = ui.textInput { feedback -> responseAction(task, "Revising...", formHandle(), formText) { - feedback(ui.newTask(), feedback, request, response) + feedback(task, feedback, request, response) } } @@ -179,14 +190,7 @@ open class CodingAgent( request: CodingActor.CodeRequest, formText: StringBuilder, formHandle: () -> StringBuilder - ) = ui.hrefLink("â™»", "href-link regen-button"){ - responseAction(task, "Regenerating...", formHandle(), formText) { - displayCode( - ui.newTask(), - request.copy(messages = request.messages.dropLastWhile { it.second == ApiModel.Role.assistant }) - ) - } - } + ) = "" protected fun playButton( task: SessionTask, @@ -197,7 +201,7 @@ open class CodingAgent( ) = if (!canPlay) "" else ui.hrefLink("â–¶", "href-link play-button"){ responseAction(task, "Running...", formHandle(), formText) { - execute(ui.newTask(), response, request) + execute(task, response, request) } } @@ -240,7 +244,7 @@ open class CodingAgent( ) { try { task.echo(renderMarkdown(feedback)) - displayCode(task, codeRequest( + newRetryable(task, codeRequest( messages = request.messages + listOf( response.code to ApiModel.Role.assistant, @@ -318,19 +322,19 @@ open class CodingAgent( resultValue.isBlank() || resultValue.trim().lowercase() == "null" -> """ |# Output |```text - |${resultOutput?.let { escapeHtml4(it).indent(" ") }} + |${resultOutput.let { /*escapeHtml4*/(it).indent(" ") }} |``` """.trimMargin() else -> """ |# Result |``` - |${resultValue?.let { escapeHtml4(it).indent(" ") }} + |${resultValue.let { /*escapeHtml4*/(it).indent(" ") }} |``` | |# Output |```text - |${resultOutput?.let { escapeHtml4(it).indent(" ") }} + |${resultOutput.let { /*escapeHtml4*/(it).indent(" ") }} |``` """.trimMargin() } diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingSubAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingSubAgent.kt new file mode 100644 index 00000000..0bc1b3fc --- /dev/null +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/CodingSubAgent.kt @@ -0,0 +1,69 @@ +package com.simiacryptus.skyenet.apps.coding + +import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.skyenet.core.actors.CodingActor +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.StorageInterface +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.interpreter.Interpreter +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.session.SessionTask +import java.util.concurrent.Semaphore +import kotlin.reflect.KClass + +open class CodingSubAgent( + api: API, + dataStorage: StorageInterface, + session: Session, + user: User?, + ui: ApplicationInterface, + interpreter: KClass, + symbols: Map, + temperature: Double = 0.1, + details: String? = null, + model: ChatModels, + mainTask: SessionTask = ui.newTask(), + val semaphore: Semaphore = Semaphore(0), +) : CodingAgent( + api = api, + dataStorage = dataStorage, + session = session, + user = user, + ui = ui, + interpreter = interpreter, + symbols = symbols, + temperature = temperature, + details = details, + model = model, + mainTask = mainTask +) { + override fun displayFeedback(task: SessionTask, request: CodingActor.CodeRequest, response: CodingActor.CodeResult) { + val formText = StringBuilder() + var formHandle: StringBuilder? = null + formHandle = task.add( + """ + |
+ |${if (!super.canPlay) "" else super.playButton(task, request, response, formText) { formHandle!! }} + |${acceptButton(task, request, response, formText) { formHandle!! }} + |
+ |${super.reviseMsg(task, request, response, formText) { formHandle!! }} + """.trimMargin(), className = "reply-message" + ) + formText.append(formHandle.toString()) + formHandle.toString() + task.complete() + } + + protected fun acceptButton( + task: SessionTask, + request: CodingActor.CodeRequest, + response: CodingActor.CodeResult, + formText: StringBuilder, + formHandle: () -> StringBuilder + ) = if (!canPlay) "" else + ui.hrefLink("\uD83D\uDE80", "href-link play-button"){ + semaphore.release() + } + +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/ShellToolAgent.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/ShellToolAgent.kt index c4cb7095..cdc2aec2 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/ShellToolAgent.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/coding/ShellToolAgent.kt @@ -24,7 +24,6 @@ import com.simiacryptus.skyenet.webui.util.OpenAPI import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.apache.commons.text.StringEscapeUtils.escapeHtml4 import org.eclipse.jetty.server.Request import org.eclipse.jetty.server.Response import org.eclipse.jetty.webapp.WebAppClassLoader @@ -61,7 +60,8 @@ abstract class ShellToolAgent( model = model ) ), -) : CodingAgent(api, dataStorage, session, user, ui, interpreter, symbols, temperature, details, model, actorMap) { + mainTask: SessionTask = ui.newTask(), +) : CodingAgent(api, dataStorage, session, user, ui, interpreter, symbols, temperature, details, model, mainTask, actorMap) { override fun displayFeedback(task: SessionTask, request: CodingActor.CodeRequest, response: CodeResult) { @@ -70,14 +70,7 @@ abstract class ShellToolAgent( formHandle = task.add( """ |
- |${ - if (!super.canPlay) "" else - super.ui.hrefLink("â–¶", "href-link play-button"){ - super.responseAction(task, "Running...", formHandle!!, formText) { - super.execute(super.ui.newTask(), response, request) - } - } - } + |${if (!canPlay) "" else playButton(task, request, response, formText) { formHandle!! }} |${super.regenButton(task, request, formText) { formHandle!! }} |${createToolButton(task, request, response, formText) { formHandle!! }} |
@@ -91,24 +84,6 @@ abstract class ShellToolAgent( private var lastResult: String? = null - override fun execute( - task: SessionTask, - response: CodeResult, - request: CodingActor.CodeRequest, - ) { - try { - lastResult = execute(task, response) - displayFeedback(task, CodingActor.CodeRequest( - messages = request.messages + - listOf( - "Running...\n\n$lastResult" to ApiModel.Role.assistant, - ).filter { it.first.isNotBlank() } - ), response) - } catch (e: Throwable) { - handleExecutionError(e, task, request, response) - } - } - private fun createToolButton( task: SessionTask, request: CodingActor.CodeRequest, @@ -210,7 +185,7 @@ abstract class ShellToolAgent( |${e.errors.joinToString("\n") { "ERROR:" + it.toString() }} |${e.warnings.joinToString("\n") { "WARN:" + it.toString() }} """.trimIndent() - task.hideable(ui, renderMarkdown("```\n${error?.let { escapeHtml4(it).indent(" ") }}\n```")) + task.hideable(ui, renderMarkdown("```\n${error?.let { /*escapeHtml4*/(it).indent(" ") }}\n```")) openAPI = openAPIParsedActor().answer( listOf( servletImpl, @@ -319,6 +294,11 @@ abstract class ShellToolAgent( } + /** + * + * TODO: This method seems redundant. + * + * */ private fun displayCodeFeedback( task: SessionTask, actor: CodingActor, @@ -326,7 +306,7 @@ abstract class ShellToolAgent( response: CodeResult = execWrap { actor.answer(request, api = api) }, onComplete: (String) -> Unit ) { - task.hideable(ui, renderMarkdown("```kotlin\n${response.code?.let { escapeHtml4(it).indent(" ") }}\n```")) + task.hideable(ui, renderMarkdown("```kotlin\n${response.code?.let { /*escapeHtml4*/(it).indent(" ") }}\n```")) val formText = StringBuilder() var formHandle: StringBuilder? = null formHandle = task.add( @@ -454,7 +434,7 @@ abstract class ShellToolAgent( ) // if ```html unwrap if (testPage.contains("```html")) testPage = testPage.substringAfter("```html").substringBefore("```") - task.add(renderMarkdown("```html\n${testPage?.let { escapeHtml4(it).indent(" ") }}\n```")) + task.add(renderMarkdown("```html\n${testPage?.let { /*escapeHtml4*/(it).indent(" ") }}\n```")) task.complete( "