Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.simiacryptus.skyenet.apps.general.WebDevApp.kt Maven / Gradle / Ivy
package com.simiacryptus.skyenet.apps.general
import com.simiacryptus.diff.addApplyFileDiffLinks
import com.simiacryptus.jopenai.API
import com.simiacryptus.jopenai.ApiModel
import com.simiacryptus.jopenai.ApiModel.Role
import com.simiacryptus.jopenai.describe.Description
import com.simiacryptus.jopenai.models.ChatModels
import com.simiacryptus.jopenai.models.ImageModels
import com.simiacryptus.jopenai.proxy.ValidatedObject
import com.simiacryptus.jopenai.util.ClientUtil.toContentList
import com.simiacryptus.jopenai.util.JsonUtil
import com.simiacryptus.skyenet.AgentPatterns
import com.simiacryptus.skyenet.Discussable
import com.simiacryptus.skyenet.TabbedDisplay
import com.simiacryptus.skyenet.core.actors.*
import com.simiacryptus.skyenet.core.platform.ClientManager
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.webui.application.ApplicationInterface
import com.simiacryptus.skyenet.webui.application.ApplicationServer
import com.simiacryptus.skyenet.webui.session.SessionTask
import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown
import org.intellij.lang.annotations.Language
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.file.Path
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicReference
import javax.imageio.ImageIO
import kotlin.io.path.name
open class WebDevApp(
applicationName: String = "Web Dev Assistant v1.2",
open val symbols: Map = mapOf(),
val temperature: Double = 0.1,
) : ApplicationServer(
applicationName = applicationName,
path = "/webdev",
) {
override fun userMessage(
session: Session,
user: User?,
userMessage: String,
ui: ApplicationInterface,
api: API
) {
val settings = getSettings(session, user) ?: Settings()
(api as ClientManager.MonitoredClient).budget = settings.budget ?: 2.00
WebDevAgent(
api = api,
dataStorage = dataStorage,
session = session,
user = user,
ui = ui,
tools = settings.tools,
model = settings.model,
parsingModel = settings.parsingModel,
root = root,
).start(
userMessage = userMessage,
)
}
data class Settings(
val budget: Double? = 2.00,
val tools: List = emptyList(),
val model: ChatModels = ChatModels.GPT4o,
val parsingModel: ChatModels = ChatModels.GPT35Turbo,
)
override val settingsClass: Class<*> get() = Settings::class.java
@Suppress("UNCHECKED_CAST")
override fun initSettings(session: Session): T? = Settings() as T
}
class WebDevAgent(
val api: API,
dataStorage: StorageInterface,
session: Session,
user: User?,
val ui: ApplicationInterface,
val model: ChatModels,
val parsingModel: ChatModels,
val tools: List = emptyList(),
@Language("Markdown") val actorMap: Map> = mapOf(
ActorTypes.ArchitectureDiscussionActor to ParsedActor(
// parserClass = PageResourceListParser::class.java,
resultClass = ProjectSpec::class.java,
prompt = """
Translate the user's idea into a detailed architecture for a simple web application.
List all html, css, javascript, and image files to be created, and for each file:
1. Mark with filename tags.
2. Describe the public interface / interaction with other components.
3. Core functional requirements.
Specify user interactions and how the application will respond to them.
Identify key HTML classes and element IDs that will be used to bind the application to the HTML.
""".trimIndent(),
model = model,
parsingModel = parsingModel,
),
ActorTypes.CodeReviewer to SimpleActor(
prompt = """
|Analyze the code summarized in the user's header-labeled code blocks.
|Review, look for bugs, and provide fixes.
|Provide implementations for missing functions.
|
|Response should use one or more code patches in diff format within ```diff code blocks.
|Each diff should be preceded by a header that identifies the file being modified.
|The diff format should use + for line additions, - for line deletions.
|The diff should include 2 lines of context before and after every change.
|
|Example:
|
|Here are the patches:
|
|### src/utils/exampleUtils.js
|```diff
| // Utility functions for example feature
| const b = 2;
| function exampleFunction() {
|- return b + 1;
|+ return b + 2;
| }
|```
|
|### tests/exampleUtils.test.js
|```diff
| // Unit tests for exampleUtils
| const assert = require('assert');
| const { exampleFunction } = require('../src/utils/exampleUtils');
|
| describe('exampleFunction', () => {
|- it('should return 3', () => {
|+ it('should return 4', () => {
| assert.equal(exampleFunction(), 3);
| });
| });
|```
""".trimMargin(),
model = model,
),
ActorTypes.HtmlCodingActor to SimpleActor(
prompt = """
You will translate the user request into a skeleton HTML file for a rich javascript application.
The html file can reference needed CSS and JS files, which are will be located in the same directory as the html file.
Do not output the content of the resource files, only the html file.
""".trimIndent(), model = model
),
ActorTypes.JavascriptCodingActor to SimpleActor(
prompt = """
You will translate the user request into a javascript file for use in a rich javascript application.
""".trimIndent(), model = model
),
ActorTypes.CssCodingActor to SimpleActor(
prompt = """
You will translate the user request into a CSS file for use in a rich javascript application.
""".trimIndent(), model = model
),
ActorTypes.EtcCodingActor to SimpleActor(
prompt = """
You will translate the user request into a file for use in a web application.
""".trimIndent(),
model = model,
),
ActorTypes.ImageActor to ImageActor(
prompt = """
You will translate the user request into an image file for use in a web application.
""".trimIndent(),
textModel = model,
imageModel = ImageModels.DallE3,
),
),
val root: File,
) : ActorSystem(actorMap.map { it.key.name to it.value }.toMap(), dataStorage, user, session) {
enum class ActorTypes {
HtmlCodingActor,
JavascriptCodingActor,
CssCodingActor,
ArchitectureDiscussionActor,
CodeReviewer,
EtcCodingActor,
ImageActor,
}
private val architectureDiscussionActor by lazy { getActor(ActorTypes.ArchitectureDiscussionActor) as ParsedActor }
private val htmlActor by lazy { getActor(ActorTypes.HtmlCodingActor) as SimpleActor }
private val imageActor by lazy { getActor(ActorTypes.ImageActor) as ImageActor }
private val javascriptActor by lazy { getActor(ActorTypes.JavascriptCodingActor) as SimpleActor }
private val cssActor by lazy { getActor(ActorTypes.CssCodingActor) as SimpleActor }
private val codeReviewer by lazy { getActor(ActorTypes.CodeReviewer) as SimpleActor }
private val etcActor by lazy { getActor(ActorTypes.EtcCodingActor) as SimpleActor }
private val codeFiles = mutableSetOf()
fun start(
userMessage: String,
) {
val task = ui.newTask()
val toInput = { it: String -> listOf(it) }
val architectureResponse = Discussable(
task = task,
userMessage = { userMessage },
initialResponse = { it: String -> architectureDiscussionActor.answer(toInput(it), api = api) },
outputFn = { design: ParsedResponse ->
// renderMarkdown("${design.text}\n\n```json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\n```")
AgentPatterns.displayMapInTabs(
mapOf(
"Text" to renderMarkdown(design.text, ui = ui),
"JSON" to renderMarkdown(
"```json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\n```",
ui = ui
),
)
)
},
ui = ui,
reviseResponse = { userMessages: List> ->
architectureDiscussionActor.respond(
messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) }
.toTypedArray()),
input = toInput(userMessage),
api = api
)
},
atomicRef = AtomicReference(),
semaphore = Semaphore(0),
heading = userMessage
).call()
try {
// val toolSpecs = tools.map { ToolServlet.tools.find { t -> t.path == it } }
// .joinToString("\n\n") { it?.let { JsonUtil.toJson(it.openApiDescription) } ?: "" }
// var messageWithTools = userMessage
// if (toolSpecs.isNotBlank()) messageWithTools += "\n\nThese services are available:\n$toolSpecs"
task.echo(
renderMarkdown(
"```json\n${JsonUtil.toJson(architectureResponse.obj)/*.indent(" ")*/}\n```",
ui = ui
)
)
val fileTabs = TabbedDisplay(task)
architectureResponse.obj.files.filter {
!it.name!!.startsWith("http")
}.map { (path, description) ->
val task = ui.newTask(false).apply { fileTabs[path.toString()] = placeholder }
task.header("Drafting $path")
codeFiles.add(File(path).toPath())
pool.submit {
when (path!!.split(".").last().lowercase()) {
"js" -> draftResourceCode(
task = task,
request = javascriptActor.chatMessages(
listOf(
// messageWithTools,
architectureResponse.text,
"Render $path - $description"
)
),
actor = javascriptActor,
path = File(path).toPath(), "js", "javascript"
)
"css" -> draftResourceCode(
task = task,
request = cssActor.chatMessages(
listOf(
// messageWithTools,
architectureResponse.text,
"Render $path - $description"
)
),
actor = cssActor,
path = File(path).toPath()
)
"html" -> draftResourceCode(
task = task,
request = htmlActor.chatMessages(
listOf(
// messageWithTools,
architectureResponse.text,
"Render $path - $description"
)
),
actor = htmlActor,
path = File(path).toPath()
)
"png" -> draftImage(
task = task,
request = etcActor.chatMessages(
listOf(
// messageWithTools,
architectureResponse.text,
"Render $path - $description"
)
),
actor = imageActor,
path = File(path).toPath()
)
"jpg" -> draftImage(
task = task,
request = etcActor.chatMessages(
listOf(
// messageWithTools,
architectureResponse.text,
"Render $path - $description"
)
),
actor = imageActor,
path = File(path).toPath()
)
else -> draftResourceCode(
task = task,
request = etcActor.chatMessages(
listOf(
// messageWithTools,
architectureResponse.text,
"Render $path - $description"
)
),
actor = etcActor,
path = File(path).toPath()
)
}
}
}.toTypedArray().forEach { it.get() }
// Apply codeReviewer
iterateCode(task)
} catch (e: Throwable) {
log.warn("Error", e)
task.error(ui, e)
}
}
fun codeSummary() = codeFiles.filter {
if (it.name.lowercase().endsWith(".png")) return@filter false
if (it.name.lowercase().endsWith(".jpg")) return@filter false
true
}.joinToString("\n\n") { path ->
"# $path\n```${path.toString().split('.').last()}\n${root.resolve(path.toFile()).readText()}\n```"
}
private fun iterateCode(
task: SessionTask
) {
Discussable(
task = task,
heading = "Code Refinement",
userMessage = { codeSummary() },
initialResponse = {
codeReviewer.answer(listOf(it), api = api)
},
outputFn = { code ->
renderMarkdown(
ui.socketManager!!.addApplyFileDiffLinks(
root = root.toPath(),
response = code,
handle = { newCodeMap ->
newCodeMap.forEach { (path, newCode) ->
task.complete("$path Updated")
}
},
ui = ui,
api = api
)
)
},
ui = ui,
reviseResponse = { userMessages ->
val userMessages = userMessages.toMutableList()
userMessages.set(0, userMessages.get(0).copy(first = codeSummary()))
val combinedMessages =
userMessages.map { ApiModel.ChatMessage(Role.user, it.first.toContentList()) }
codeReviewer.respond(
input = listOf(element = combinedMessages.joinToString("\n")),
api = api,
messages = combinedMessages.toTypedArray(),
)
},
).call()
}
private fun draftImage(
task: SessionTask,
request: Array,
actor: ImageActor,
path: Path,
) {
try {
var code = Discussable(
task = task,
userMessage = { "" },
heading = "Drafting $path",
initialResponse = {
val messages = (request + ApiModel.ChatMessage(Role.user, "Draft $path".toContentList()))
.toList().toTypedArray()
actor.respond(
listOf(request.joinToString("\n") { it.content?.joinToString() ?: "" }),
api,
*messages
)
},
outputFn = { img ->
renderMarkdown(
" ", ui = ui
)
},
ui = ui,
reviseResponse = { userMessages: List> ->
actor.respond(
messages = (request.toList() + userMessages.map {
ApiModel.ChatMessage(
it.second,
it.first.toContentList()
)
})
.toTypedArray(),
input = listOf(element = (request.toList() + userMessages.map {
ApiModel.ChatMessage(
it.second,
it.first.toContentList()
)
})
.joinToString("\n") { it.content?.joinToString() ?: "" }),
api = api,
)
},
).call()
task.complete(
renderMarkdown(
" ", ui = ui
)
)
} catch (e: Throwable) {
val error = task.error(ui, e)
task.complete(ui.hrefLink("♻", "href-link regen-button") {
error?.clear()
draftImage(task, request, actor, path)
})
}
}
private fun write(
code: ImageResponse,
path: Path
): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
ImageIO.write(
code.image,
path.toString().split(".").last(),
byteArrayOutputStream
)
val bytes = byteArrayOutputStream.toByteArray()
return bytes
}
private fun draftResourceCode(
task: SessionTask,
request: Array,
actor: SimpleActor,
path: Path,
vararg languages: String = arrayOf(path.toString().split(".").last().lowercase()),
) {
try {
var code = Discussable(
task = task,
userMessage = { "Drafting $path" },
heading = "",
initialResponse = {
actor.respond(
listOf(request.joinToString("\n") { it.content?.joinToString() ?: "" }),
api,
*(request + ApiModel.ChatMessage(Role.user, "Draft $path".toContentList()))
.toList().toTypedArray()
)
},
outputFn = { design: String ->
var design = design
languages.forEach { language ->
if (design.contains("```$language")) {
design = design.substringAfter("```$language").substringBefore("```")
}
}
renderMarkdown("```${languages.first()}\n${design.let { it }}\n```", ui = ui)
},
ui = ui,
reviseResponse = { userMessages: List> ->
actor.respond(
messages = (request.toList() + userMessages.map {
ApiModel.ChatMessage(
it.second,
it.first.toContentList()
)
})
.toTypedArray(),
input = listOf(element = (request.toList() + userMessages.map {
ApiModel.ChatMessage(
it.second,
it.first.toContentList()
)
})
.joinToString("\n") { it.content?.joinToString() ?: "" }),
api = api,
)
},
).call()
code = extractCode(code)
task.complete(
"$path Updated"
)
} catch (e: Throwable) {
val error = task.error(ui, e)
task.complete(ui.hrefLink("♻", "href-link regen-button") {
error?.clear()
draftResourceCode(task, request, actor, path, *languages)
})
}
}
private fun extractCode(code: String): String {
var code = code
code = code.trim()
"(?s)```[^\\n]*\n(.*)\n```".toRegex().find(code)?.let {
code = it.groupValues[1]
}
return code
}
companion object {
val log = org.slf4j.LoggerFactory.getLogger(WebDevAgent::class.java)
data class ProjectSpec(
@Description("Files in the project design, including all local html, css, and js files.")
val files: List = emptyList()
) : ValidatedObject {
override fun validate(): String? = when {
files.isEmpty() -> "Resources are required"
files.any { it.validate() != null } -> "Invalid resource"
else -> null
}
}
data class ProjectFile(
@Description("The path to the file, relative to the project root.")
val name: String? = "",
@Description("A brief description of the file's purpose and contents.")
val description: String? = ""
) : ValidatedObject {
override fun validate(): String? = when {
name.isNullOrBlank() -> "Path is required"
name.contains(" ") -> "Path cannot contain spaces"
!name.contains(".") -> "Path must contain a file extension"
else -> null
}
}
}
}