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.models.ApiModel
import com.simiacryptus.jopenai.models.ApiModel.Role
import com.simiacryptus.jopenai.ChatClient
import com.simiacryptus.jopenai.describe.Description
import com.simiacryptus.jopenai.models.ChatModels
import com.simiacryptus.jopenai.models.ImageModels
import com.simiacryptus.jopenai.models.OpenAIModels
import com.simiacryptus.jopenai.proxy.ValidatedObject
import com.simiacryptus.jopenai.util.ClientUtil.toContentList
import com.simiacryptus.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.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.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 ChatClient).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 = OpenAIModels.GPT4o,
val parsingModel: ChatModels = OpenAIModels.GPT4oMini,
)
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.
""".trimMargin(),
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.
""".trimMargin(), model = model
),
ActorTypes.JavascriptCodingActor to SimpleActor(
prompt = """
|You will translate the user request into a javascript file for use in a rich javascript application.
""".trimMargin(), model = model
),
ActorTypes.CssCodingActor to SimpleActor(
prompt = """
|You will translate the user request into a CSS file for use in a rich javascript application.
""".trimMargin(), model = model
),
ActorTypes.EtcCodingActor to SimpleActor(
prompt = """
|You will translate the user request into a file for use in a web application.
""".trimMargin(),
model = model,
),
ActorTypes.ImageActor to ImageActor(
prompt = """
|You will translate the user request into an image file for use in a web application.
""".trimMargin(),
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
}
}
}
}