TutorialCodeTools.kt
src/jvmMain/kotlin/afield/ai/tools/TutorialCodeTools.kt · 125 lines
package afield.ai.tools
import afield.ai.retrieval.SemanticRetrievalHit
import com.fasterxml.jackson.annotation.JsonClassDescription
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import org.yaml.snakeyaml.Yaml
import java.io.File
Finding tutorial code: a worked example of box code to start from.
Field ships about 260 tutorial documents: the materialized agent transcripts under
public/docs/assets/agent_transcripts/<name>/ and the graphics curriculum in
public/docs/assets/canonical_graphics/. Semantic search indexes them box by box as its tutorials corpus,
the way it indexes a workspace; this tool is a query on that corpus, grouped by document (by lesson for the
graphics curriculum, whose boxes are its lessons), with each document's description and docs links from its
transcript and the code files to read. A match names files rather than quoting them: read one with
AttachmentRequest to see the box code itself.
data class TutorialCodeSearchRequest
Ask for tutorials by what they do. The answer is a TutorialCodeSearchResult.
@JsonClassDescription("Find Field's tutorial code: about 260 worked examples of box code, by what they do, e.g. query = `draw FLines`, `GLSL compute shader on the renderer`, `slider controls`. Returns each match's name, what it shows, the docs pages it illustrates, and its box code files: read one with AttachmentRequest(file=...) to see working box code before writing your own. If nothing matches it fails with success=false and suggests plainer words; it is unavailable when semantic search is not running. Prefer this to guessing an API from Field's source.")
@AgentToolMeta("search", effects = [ToolEffect.FILESYSTEM_READ])
data class TutorialCodeSearchRequest(
/** What to make or learn, in plain words, matched against the `tutorials` corpus rather than by keyword. */
@param:JsonPropertyDescription("What you want to make or learn, in plain words.")
val query: String,
/** How many tutorials to return; anything outside 1..12 is clamped into it. */
@param:JsonPropertyDescription("Tutorials to return, 1..12.")
val limit: Int = 6
)
data class TutorialCodeMatch
One tutorial that matched, with the files a reader would open next.
data class TutorialCodeMatch(
/** The tutorial document's name, or `canonical_graphics` and the lesson box's name for one lesson. */
val name: String,
/** What the document's transcript says it shows, to 400 characters, empty when it says nothing. */
val description: String,
/** Names of the boxes inside it that matched, up to six. */
val boxes: List<String>,
/** The docs pages it illustrates, each as `page: why` from the transcript, up to four. */
val docs: List<String>,
/** Paths of its box code and shader files, up to eight, for [AttachmentRequest] to read. */
val codeFiles: List<String>
)
data class TutorialCodeSearchResult(val success: Boolean, val message: String, val matches: List<TutorialCodeMatch>)
The tool's answer: success is false both for a failed search and for a search that matched nothing.
data class TutorialCodeSearchResult(val success: Boolean, val message: String, val matches: List<TutorialCodeMatch>)
internal object TutorialCode
internal object TutorialCode {
private val codeExtensions = setOf("kt", "glsl", "slang", "metal", "js", "py")
data class About(val description: String, val docs: List<String>)
@Volatile private var cached: Pair<Long, Map<String, About>>? = null
private fun transcriptsDir(): File? {
var dir: File? = File(System.getProperty("user.dir")).absoluteFile
while (dir != null) {
File(dir, "dev_docs/agents/transcripts").takeIf { it.isDirectory }?.let { return it }
dir = dir.parentFile
}
return null
}
fun about(): Map<String, About>
What each tutorial's transcript says it shows, and the docs pages it illustrates.
fun about(): Map<String, About> {
val dir = transcriptsDir() ?: return emptyMap()
cached?.takeIf { it.first == dir.lastModified() }?.let { return it.second }
val out = dir.listFiles { f -> f.extension == "yaml" }.orEmpty().associate { file ->
val parsed = runCatching { Yaml().load<Any?>(file.readText()) as? Map<*, *> }.getOrNull().orEmpty()
file.nameWithoutExtension to About(
parsed["description"]?.toString()?.trim().orEmpty(),
(parsed["docs"] as? List<*>).orEmpty().mapNotNull { d ->
(d as? Map<*, *>)?.let { m -> listOfNotNull(m["page"]?.toString(), m["why"]?.toString()).joinToString(": ") }
})
} + ("canonical_graphics" to About(
"The Field 4 (Vulkan) graphics curriculum: lessons that run against a live renderer, from a first triangle to compute " +
"shaders, particles, texturing, render-to-texture and GPU sorting. Each lesson runs as a child of the `window` box.",
listOf("assets/canonical_graphics/LESSONS.md: the lesson index, with each lesson's parent chain and files",
"afield_four_graphics.md: the walkthrough")))
cached = dir.lastModified() to out
return out
}
fun group(hits: List<SemanticRetrievalHit>, limit: Int, about: Map<String, About> = about()): List<TutorialCodeMatch>
Hits grouped into tutorials, best first: by document, or by box in the graphics curriculum.
fun group(hits: List<SemanticRetrievalHit>, limit: Int, about: Map<String, About> = about()): List<TutorialCodeMatch> {
val groups = LinkedHashMap<String, MutableList<SemanticRetrievalHit>>()
hits.filter { it.documentName.isNotBlank() }.forEach { hit ->
val key = if (hit.documentName == "canonical_graphics") "canonical_graphics: ${hit.boxName.ifBlank { hit.boxId }}" else hit.documentName
groups.getOrPut(key) { mutableListOf() } += hit
}
return groups.entries.take(limit.coerceIn(1, 12)).map { (name, rows) ->
val document = rows.first().documentName
val info = about[document]
// Every text file of each matched box: the Kotlin and the shaders it draws with.
// Every text file of each matched box (its Kotlin and the shaders it draws with); a tutorial's summary
// stands for the whole document, so its hit brings the document's code files.
val files = rows.distinctBy { it.boxId to it.path }.flatMap { hit ->
val wholeDocument = hit.path.endsWith("/")
val dirPath = if (wholeDocument) hit.path.trimEnd('/') else hit.path.substringBeforeLast('/', "")
val siblings = File(System.getProperty("user.dir"), dirPath)
.listFiles { f -> f.name.contains("_.") && f.extension in codeExtensions && (wholeDocument || f.name.startsWith("${hit.boxId}_")) }.orEmpty()
siblings.map { "$dirPath/${it.name}" }.sorted().ifEmpty { if (wholeDocument) emptyList() else listOf(hit.path) }
}.distinct()
TutorialCodeMatch(name, info?.description.orEmpty().take(400),
rows.mapNotNull { it.boxName.takeIf(String::isNotBlank) }.distinct().take(6), info?.docs.orEmpty().take(4), files.take(8))
}
}
}
internal fun registerTutorialCodeTools(tools: ToolRegistry)
internal fun registerTutorialCodeTools(tools: ToolRegistry) {
tools.register(TutorialCodeSearchRequest::class.java, { it.semanticRetrievalSearch != null }) { ctx, req ->
val search = ctx.semanticRetrievalSearch!!(SemanticRetrievalSearchRequest(req.query, limit = 40, corpora = listOf("tutorials")))
if (!search.success) return@register TutorialCodeSearchResult(false, search.message, emptyList())
val matches = TutorialCode.group(search.results, req.limit)
TutorialCodeSearchResult(matches.isNotEmpty(),
if (matches.isEmpty()) "No tutorial code matched '${req.query}'. Try plainer words (what you want to see), or SemanticRetrievalSearchRequest with corpora=[documentation]."
else "${matches.size} tutorial(s). Read a codeFiles entry with AttachmentRequest to see working box code.",
matches)
}
}