diff --git a/docs/app_best_practices.md b/docs/app_best_practices.md new file mode 100644 index 00000000..8aa3a094 --- /dev/null +++ b/docs/app_best_practices.md @@ -0,0 +1,392 @@ +Here's a best practices document for developing apps in this project: + +# Best Practices for SkyeNet Apps + +## Application Architecture + +### Core Design Principles + +1. Single Responsibility: + +```kotlin +// Each class should have a single, well-defined purpose +class DocumentProcessor( + val parser: DocumentParser, + val validator: DocumentValidator, + val storage: DocumentStorage +) +``` + +2. Dependency Injection: + +```kotlin +class MyApp( + private val api: API, + private val storage: StorageService, + private val validator: ValidationService +) : ApplicationServer { + // Dependencies are injected rather than created internally +} +``` + +### Application Structure + +1. Separate core logic from UI: + +```kotlin +class MyApp( + // Core configuration + val settings: Settings, + val model: ChatModel, + // UI configuration + applicationName: String = "My App", + path: String = "/myapp" +) : ApplicationServer(applicationName, path) { + // Core business logic methods + private fun processData() {} + + // UI handling methods + override fun userMessage() {} +} +``` + +2. Use immutable data classes for configuration: + +```kotlin +data class Settings( + val maxItems: Int = 100, + val timeout: Duration = Duration.ofMinutes(5), + val features: Set = setOf(), + val retryConfig: RetryConfig = RetryConfig(), + val validationRules: List = listOf() +) +data class RetryConfig( + val maxAttempts: Int = 3, + val backoffMs: Long = 1000 +) +``` + +3. Handle state management: + +```kotlin +// Per-session state +private val sessionState = mutableMapOf() +// Thread-safe state updates +private val stateGuard = AtomicBoolean(false) + +// Immutable state updates +fun updateState(sessionId: String, update: (SessionState) -> SessionState) { + synchronized(stateGuard) { + sessionState[sessionId] = update(sessionState[sessionId] ?: SessionState()) + } +} +``` + +## Logging Best Practices + +### Logging Guidelines + +1. Use consistent log formats: + +```kotlin +private fun logEvent( + event: String, + data: Map, + level: LogLevel = LogLevel.INFO +) { + when (level) { + LogLevel.DEBUG -> log.debug("$event: ${data.toJson()}") + LogLevel.INFO -> log.info("$event: ${data.toJson()}") + LogLevel.WARN -> log.warn("$event: ${data.toJson()}") + LogLevel.ERROR -> log.error("$event: ${data.toJson()}") + } +} +``` + +### Sub-Log Creation + +1. Create child API clients with dedicated logs for each major operation: + +```kotlin +val api = (api as ChatClient).getChildClient().apply { + val createFile = task.createFile(".logs/api-${UUID.randomUUID()}.log") + createFile.second?.apply { + logStreams += this.outputStream().buffered() + task.verbose("API log: $this") + } +} +``` + +### Structured Logging + +```kotlin +log.info( + "Processing request", mapOf( + "userId" to user.id, + "requestType" to requestType, + "timestamp" to System.currentTimeMillis(), + "context" to mapOf( + "session" to session.id, + "environment" to env, + "features" to enabledFeatures + ) + ) +) +``` + +2. Use appropriate log levels: + +```kotlin +log.debug("Fine-grained diagnostic info") +log.info("General operational events") +log.warn("Potentially harmful situations") +log.error("Error events that might still allow the app to continue") +// Add context to error logs +log.error( + "Operation failed", mapOf( + "error" to e.message, + "stackTrace" to e.stackTraceToString(), + "context" to operationContext + ) +) +``` + +## Resource Management + +### API Client Lifecycle + +```kotlin +// Use structured resource management +inline fun withAPI(crossinline block: (API) -> T): T { + return api.use { client -> + try { + block(client) + } finally { + client.close() + } + } +} + +api.use { client -> + try { + // Use API client + } finally { + client.close() + } +} +``` + +### Memory Management + +```kotlin +// Use sequences for large collections and implement pagination +files.asSequence() + .filter { it.length() < maxSize } + .map { process(it) } + .take(limit) + .chunked(pageSize) + .toList() +// Implement resource pooling +val resourcePool = ResourcePool( + maxSize = 10, + factory = { createExpensiveResource() } +) + +// Clear buffers after use +buffer.clear() +``` + +3. Include contextual information in logs: + +```kotlin +log.info("Processing user message: $userMessage") +log.error("Error processing task ${taskId}", exception) +``` + +## Exception Handling + +### General Exception Handling Pattern + +```kotlin +try { + // Main operation + checkPreconditions() + validateInput(data) +} catch (e: SocketTimeoutException) { + log.error("Network timeout", e) + task.add("The operation timed out. Please check your network connection.") +} catch (e: IOException) { + log.error("I/O error", e) + task.add("An I/O error occurred. Please try again later.") +} catch (e: IllegalStateException) { + log.error("Invalid state", e) + task.add("Operation cannot be completed in current state") +} catch (e: Exception) { + log.error("Unexpected error", e) + task.error(ui, e) +} finally { + cleanup() + task.complete() +} +``` + +### Input Validation + +```kotlin +fun validate(input: UserInput) { + require(input.name.isNotBlank()) { "Name cannot be empty" } + require(input.age in 0..150) { "Invalid age" } + check(isInitialized) { "System not initialized" } +} +``` + +### User-Friendly Error Messages + +```kotlin +when (e) { + is IllegalArgumentException -> task.add("Invalid input: ${e.message}") + is IllegalStateException -> task.add("Operation failed: ${e.message}") + is SecurityException -> task.add("Access denied: ${e.message}") + else -> task.add("An unexpected error occurred. Please try again later.") +} +``` + +## Using Retry and Tabs + +### Retryable Operations + +1. Wrap retryable operations using the Retryable class: + +```kotlin +Retryable(ui, task) { content -> + try { + // Operation that might need retry + val result = performOperation() + renderMarkdown(result, ui = ui) + } catch (e: Exception) { + task.error(ui, e) + "Error: ${e.message}" + } +} +``` + +### Tab Management + +1. Create organized tab displays: + +```kotlin +val tabbedDisplay = TabbedDisplay(task) +tabbedDisplay["Tab Name"] = content +tabbedDisplay.update() +``` + +2. Handle nested tabs: + +```kotlin +val parentTabs = TabbedDisplay(task) +val childTask = ui.newTask(false) +parentTabs["Parent Tab"] = childTask.placeholder +val childTabs = TabbedDisplay(childTask) +``` + +## Task Status Management + +### Task Lifecycle States + +1. Initial State: + +```kotlin +val task = ui.newTask() +task.add(SessionTask.spinner) // Show loading spinner +``` + +2. In Progress: + +```kotlin +task.add("Processing...") // Update status +task.verbose("Detailed progress info") // Show detailed progress +``` + +3. Completion: + +```kotlin +task.complete() // Normal completion +task.complete("Operation completed successfully") // Completion with message +``` + +4. Error State: + +```kotlin +task.error(ui, exception) // Show error with details +``` + +### Progress Tracking + +1. Use progress bars for long operations: + +```kotlin +val progressBar = progressBar(ui.newTask()) +progressBar.add(completedItems.toDouble(), totalItems.toDouble()) +``` + +## Best Practices for Task Organization + +1. Break down complex operations into subtasks: + +```kotlin +val mainTask = ui.newTask() +val subTask1 = ui.newTask(false) +val subTask2 = ui.newTask(false) +mainTask.verbose(subTask1.placeholder) +mainTask.verbose(subTask2.placeholder) +``` + +2. Use meaningful task headers: + +```kotlin +task.header("Processing Stage 1") +// ... operations ... +task.header("Processing Stage 2") +``` + +3. Provide visual feedback: + +```kotlin +task.add( + MarkdownUtil.renderMarkdown(""" +## Current Progress +- Step 1: Complete ✓ +- Step 2: In Progress ⟳ +- Step 3: Pending ○ +""", ui = ui + ) +) +``` + +## Memory and Resource Management + +1. Clean up resources: + +```kotlin +try { + // Use resources +} finally { + resource.close() + task.complete() +} +``` + +2. Handle large data efficiently: + +```kotlin +fun truncate(output: String, kb: Int = 32): String { + if (output.length > 1024 * 2 * kb) { + return output.substring(0, 1024 * kb) + + "\n\n... Output truncated ...\n\n" + + output.substring(output.length - 1024 * kb) + } + return output +} +``` + +Following these best practices will help ensure your apps are reliable, maintainable, and provide a good user experience. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 4d3feb2e..27f4e0e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Gradle Releases -> https://github.com/gradle/gradle/releases libraryGroup=com.simiacryptus.skyenet -libraryVersion=1.2.18 +libraryVersion=1.2.19 gradleVersion=7.6.1 kotlin.daemon.jvmargs=-Xmx4g diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt index 03b41af1..a8f48f35 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/apps/general/PatchApp.kt @@ -21,15 +21,15 @@ import com.simiacryptus.util.JsonUtil import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Path -import java.util.UUID +import java.util.* abstract class PatchApp( override val root: File, - val session: Session, - val settings: Settings, - val api: ChatClient, - val model: ChatModel, - val promptPrefix: String = """The following command was run and produced an error:""" + private val session: Session, + protected val settings: Settings, + private val api: ChatClient, + private val model: ChatModel, + private val promptPrefix: String = """The following command was run and produced an error:""" ) : ApplicationServer( applicationName = "Magic Code Fixer", path = "/fixCmd", @@ -40,6 +40,11 @@ abstract class PatchApp( const val tripleTilde = "`" + "``" // This is a workaround for the markdown parser when editing this file } + // Add structured logging + private fun logEvent(event: String, data: Map) { + log.info("$event: ${JsonUtil.toJson(data)}") + } + data class OutputResult(val exitCode: Int, val output: String) abstract fun codeFiles(): Set @@ -103,7 +108,7 @@ abstract class PatchApp( var workingDirectory: File? = null, var exitCodeOption: String = "nonzero", var additionalInstructions: String = "", - val autoFix: Boolean, + val autoFix: Boolean ) fun run( @@ -156,6 +161,13 @@ abstract class PatchApp( ui: ApplicationInterface, api: ChatClient, ) { + // Add logging for operation start + logEvent( + "Starting fix operation", mapOf( + "exitCode" to output.exitCode, + "settings" to settings + ) + ) Retryable(ui, task) { content -> fixAllInternal( settings = settings, @@ -167,6 +179,13 @@ abstract class PatchApp( ) content.clear() "" + }.also { + // Add logging for operation completion + logEvent( + "Fix operation completed", mapOf( + "success" to true + ) + ) } } @@ -374,7 +393,4 @@ abstract class PatchApp( content.append("
${MarkdownUtil.renderMarkdown(markdown!!)}
") } -} - - - +} \ No newline at end of file diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt index 7729088a..4d9bd231 100644 --- a/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt +++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/util/MarkdownUtil.kt @@ -12,22 +12,54 @@ import java.util.* object MarkdownUtil { fun renderMarkdown( - markdown: String, + rawMarkdown: String, options: MutableDataSet = defaultOptions(), tabs: Boolean = true, ui: ApplicationInterface? = null, - ): String { - if (markdown.isBlank()) return "" - val parser = Parser.builder(options).build() - val renderer = HtmlRenderer.builder(options).build() - val document = parser.parse(markdown) - val html = renderer.render(document) + ) = renderMarkdown(rawMarkdown, options, tabs, ui) { it } + + fun renderMarkdown( + rawMarkdown: String, + options: MutableDataSet = defaultOptions(), + tabs: Boolean = true, + ui: ApplicationInterface? = null, + markdownEditor: (String) -> String, + ): String { + val markdown = markdownEditor(rawMarkdown) + val stackInfo = """ +
    +${Thread.currentThread().stackTrace.joinToString("\n") { "
  1. " + it.toString() + "
  2. " }} +
+ """ + val asHtml = + stackInfo + HtmlRenderer.builder(options).build().render(Parser.builder(options).build().parse(markdown)) + .let { renderMermaid(it, ui, tabs) } + return when { + markdown.isBlank() -> "" + asHtml == rawMarkdown -> asHtml + tabs -> { + displayMapInTabs( + mapOf( + "HTML" to asHtml, + "Markdown" to """
${
+              rawMarkdown.replace(Regex("<"), "<").replace(Regex(">"), ">")
+            }
""", + "Hide" to "", + ), ui = ui + ) + } + + else -> asHtml + } + } + + private fun renderMermaid(html: String, ui: ApplicationInterface?, tabs: Boolean): String { val mermaidRegex = Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL) val matches = mermaidRegex.findAll(html) var htmlContent = html matches.forEach { match -> - var mermaidCode = match.groups[1]!!.value + val mermaidCode = match.groups[1]!!.value // HTML Decode mermaidCode val fixedMermaidCode = fixupMermaidCode(mermaidCode) var mermaidDiagramHTML = """
$fixedMermaidCode
""" @@ -46,31 +78,20 @@ object MarkdownUtil { log.warn("Failed to render Mermaid diagram", e) } val replacement = if (tabs) """ - |
- |
- | - | - |
- |
$mermaidDiagramHTML
- |
$fixedMermaidCode
- |
- |""".trimMargin() else """ - |$mermaidDiagramHTML - |""".trimMargin() + |
+ |
+ | + | + |
+ |
$mermaidDiagramHTML
+ |
$fixedMermaidCode
+ |
+ |""".trimMargin() else """ + |$mermaidDiagramHTML + |""".trimMargin() htmlContent = htmlContent.replace(match.value, replacement) } - //language=HTML - return if (tabs) { - displayMapInTabs( - mapOf( - "HTML" to htmlContent, - "Markdown" to """
${
-            markdown.replace(Regex("<"), "<").replace(Regex(">"), ">")
-          }
""", - "Hide" to "", - ), ui = ui - ) - } else htmlContent + return htmlContent } var MMDC_CMD: List = listOf("mmdc")