feat: clean display for all 37 Hermes tools in chat bubbles (#408)
* feat: clean todo tool display in chat bubbles Add todo-specific parsing in parseToolOutput that extracts the structured todos array and summary counts from the gateway payload, building a clean one-line summary and formatted item list with status markers ([x], [>], [~], [ ]). Also register 'todo' in ToolSchemaRegistry with 📋 emoji. Previously the todo tool fell back to generic rendering, showing the raw todos/summary JSON as ugly key-value pairs in the expanded view. * feat: clean fact_store tool display in chat bubbles Add fact_store-specific parsing in parseToolOutput that handles all action types: probe/search/reason/related/contradict (results[] array), list (facts[] array), and simple CRUD status (add/update/remove). Each fact is rendered as a compact block: #id content text [category] trust: 0.50 🏷️ tags Summary line shows count + action context (e.g. '4 facts (probe: entity)'). Also register 'fact_store' in ToolSchemaRegistry with 🧠 emoji. * feat: clean session_search tool display in chat bubbles Add session_search-specific parsing in parseToolOutput that handles all 4 calling modes: discover (results[] with title/snippet/bookends), scroll (messages[] with anchor marker), read (messages[] with session dump and truncation flag), and browse (results[] with recent session metadata). - Registered in ToolSchemaRegistry with U+1F50D (magnifying glass) emoji - Summary adapts per mode: "U+1F50D 3 sessions: query" / "U+1F4DC N messages (scroll)" / "U+1F4D6 N messages" / "U+1F4CB N recent sessions" - Results show date, source, title, model, matched role, snippet - Messages show role emoji (U+1F464/U+1F916/U+1F527), id, tool name, content - Anchor message gets U+2B05 marker in scroll mode - Content capped at 300 chars, snippets at 200, newlines flattened - Existing todo and fact_store formatting unchanged * feat: add registry entries + custom parsing for all remaining tools Registry entries (emoji+summaryArgKey) for 22 tools: cronjob, memory, fact_feedback, process, skill_manage, web_extract, browser_type, browser_cdp, browser_dialog, vision_analyze, tts, video_generate, image_generate, x_search, send_message, skills_list, skill_view, tool_search, tool_describe, tool_call, read_terminal, computer_use Custom formatting blocks (10 tools): memory (modes+status), cronjob (list+CRUD), web_search (results with title/url/desc), skills_list (skill cards), skill_view (content+files), process (list+poll), x_search (answer+citations+degraded), vision_analyze (description), web_extract (content), tool_search (matches) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
b9814e04c6
commit
a0954e2654
2 files changed
+766
No files matched your search
@@ -562,6 +562,615 @@ fun parseToolOutput(
|
||||
?.let { "${config.summaryPrefix}$it" }
|
||||
}
|
||||
|
||||
// ── Todo-specific formatting ──
|
||||
// Extract the structured todos array and summary object from the result,
|
||||
// build a clean one-line summary and formatted item list.
|
||||
if (resolvedToolName == "todo") {
|
||||
val todosArray =
|
||||
dataSource.get("todos")
|
||||
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||
?.asJsonArray
|
||||
val summaryObj =
|
||||
dataSource.get("summary")
|
||||
?.takeIf { !it.isJsonNull && it.isJsonObject }
|
||||
?.asJsonObject
|
||||
|
||||
val todoSummaryText =
|
||||
summaryObj?.let { s ->
|
||||
val total = s.get("total")?.asDouble?.toInt() ?: 0
|
||||
val pending = s.get("pending")?.asDouble?.toInt() ?: 0
|
||||
val inProgress = s.get("in_progress")?.asDouble?.toInt() ?: 0
|
||||
val completed = s.get("completed")?.asDouble?.toInt() ?: 0
|
||||
val cancelled = s.get("cancelled")?.asDouble?.toInt() ?: 0
|
||||
val parts = mutableListOf<String>()
|
||||
if (pending > 0) parts.add("$pending pending")
|
||||
if (inProgress > 0) parts.add("$inProgress in_progress")
|
||||
if (completed > 0) parts.add("$completed completed")
|
||||
if (cancelled > 0) parts.add("$cancelled cancelled")
|
||||
val itemWord = if (total == 1) "item" else "items"
|
||||
if (parts.isEmpty()) {
|
||||
"📋 0 items"
|
||||
} else {
|
||||
"📋 $total $itemWord (${parts.joinToString(", ")})"
|
||||
}
|
||||
}
|
||||
|
||||
val formattedTodos =
|
||||
todosArray?.mapNotNull { element ->
|
||||
if (!element.isJsonObject) return@mapNotNull null
|
||||
val item = element.asJsonObject
|
||||
val id = item.get("id")?.asString ?: ""
|
||||
val content = item.get("content")?.asString ?: ""
|
||||
val status = item.get("status")?.asString ?: "pending"
|
||||
val marker =
|
||||
when (status) {
|
||||
"completed" -> "[x]"
|
||||
"in_progress" -> "[>]"
|
||||
"cancelled" -> "[~]"
|
||||
else -> "[ ]"
|
||||
}
|
||||
"$marker $id. $content"
|
||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = todoSummaryText,
|
||||
mainOutput = formattedTodos,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Fact Store-specific formatting ──
|
||||
// Extract structured fact results (probe/search/reason/related/contradict → results[],
|
||||
// list → facts[], add/update/remove → simple status). Build a clean summary and
|
||||
// formatted fact list with category, trust score, and tags.
|
||||
if (resolvedToolName == "fact_store") {
|
||||
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val entity = argsObj?.get("entity")?.takeIf { !it.isJsonNull }?.asString
|
||||
val query = argsObj?.get("query")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
// Try both key shapes: results[] (probe/search/reason/related/contradict) and facts[] (list)
|
||||
val factsArray =
|
||||
dataSource.get("results")
|
||||
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||
?.asJsonArray
|
||||
?: dataSource.get("facts")
|
||||
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||
?.asJsonArray
|
||||
|
||||
val count = dataSource.get("count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt() ?: 0
|
||||
val status = dataSource.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||
val factId = dataSource.get("fact_id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val removed = dataSource.get("removed")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||
val updated = dataSource.get("updated")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||
|
||||
// Build summary line
|
||||
val actionContext =
|
||||
when {
|
||||
entity != null -> " ($action: $entity)"
|
||||
query != null -> " ($action: $query)"
|
||||
else -> " ($action)"
|
||||
}
|
||||
val factSummaryText =
|
||||
when {
|
||||
status == "added" && factId != null -> "🧠 Fact added (ID: $factId)"
|
||||
status == "added" -> "🧠 Fact added"
|
||||
removed == true -> "🧠 Fact removed"
|
||||
updated == true -> "🧠 Fact updated"
|
||||
factsArray != null -> "🧠 $count facts$actionContext"
|
||||
else -> "🧠 $action"
|
||||
}
|
||||
|
||||
// Format each fact as a compact block: #ID + content on first line, meta on indented second line
|
||||
val formattedFacts =
|
||||
factsArray?.mapNotNull { element ->
|
||||
if (!element.isJsonObject) return@mapNotNull null
|
||||
val item = element.asJsonObject
|
||||
val fid = item.get("fact_id")?.asDouble?.toInt() ?: 0
|
||||
val content = item.get("content")?.asString ?: ""
|
||||
val category = item.get("category")?.asString
|
||||
val trust = item.get("trust_score")?.asDouble
|
||||
val tags = item.get("tags")?.asString?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val firstLine = "#$fid $content"
|
||||
val metaParts = mutableListOf<String>()
|
||||
if (category != null) metaParts.add("[$category]")
|
||||
if (trust != null) metaParts.add("trust: ${"%.2f".format(trust)}")
|
||||
if (tags != null) metaParts.add("🏷️ $tags")
|
||||
if (metaParts.isNotEmpty()) {
|
||||
"$firstLine\n ${metaParts.joinToString(" ")}"
|
||||
} else {
|
||||
firstLine
|
||||
}
|
||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val factMainOutput =
|
||||
when {
|
||||
status == "added" && factId != null -> "✅ Fact #$factId stored"
|
||||
status == "added" -> "✅ Fact stored"
|
||||
removed == true -> "✅ Removed"
|
||||
updated == true -> "✅ Updated"
|
||||
formattedFacts != null -> formattedFacts
|
||||
else -> null
|
||||
}
|
||||
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = factSummaryText,
|
||||
mainOutput = factMainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Session Search-specific formatting ──
|
||||
// Handles all 4 calling modes: discover (results[] with snippets/bookends),
|
||||
// scroll (messages[] with anchor), read (messages[] with session dump),
|
||||
// and browse (results[] with recent session metadata).
|
||||
if (resolvedToolName == "session_search") {
|
||||
val mode = dataSource.get("mode")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val query = dataSource.get("query")?.takeIf { !it.isJsonNull }?.asString
|
||||
val resultsArray =
|
||||
dataSource.get("results")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
val messagesArray =
|
||||
dataSource.get("messages")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
val count = dataSource.get("count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val msgCount = dataSource.get("message_count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val truncated = dataSource.get("truncated")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||
|
||||
// Build summary line
|
||||
val ssSummaryText =
|
||||
when (mode) {
|
||||
"discover" -> "🔍 ${count ?: 0} session${if (count == 1) "" else "s"}${query?.let {
|
||||
": ${it.take(
|
||||
60,
|
||||
)}"
|
||||
} ?: ""}"
|
||||
"scroll" -> "📜 ${messagesArray?.size() ?: 0} messages (scroll)"
|
||||
"read" -> "📖 ${msgCount ?: messagesArray?.size() ?: 0} messages${
|
||||
if (truncated == true) " (truncated)" else ""
|
||||
}"
|
||||
"browse" -> "📋 ${count ?: 0} recent sessions"
|
||||
else -> "🔍 session_search"
|
||||
}
|
||||
|
||||
// Format results (discover / browse)
|
||||
val formattedResults =
|
||||
resultsArray?.mapIndexedNotNull { idx, element ->
|
||||
if (!element.isJsonObject) return@mapIndexedNotNull null
|
||||
val item = element.asJsonObject
|
||||
val whenField =
|
||||
item.get("when")?.takeIf { !it.isJsonNull }?.asString
|
||||
?: item.get("started_at")?.takeIf { !it.isJsonNull }?.asString
|
||||
val source = item.get("source")?.takeIf { !it.isJsonNull }?.asString
|
||||
val title = item.get("title")?.takeIf { !it.isJsonNull }?.asString
|
||||
val snippet = item.get("snippet")?.takeIf { !it.isJsonNull }?.asString
|
||||
val preview = item.get("preview")?.takeIf { !it.isJsonNull }?.asString
|
||||
val model = item.get("model")?.takeIf { !it.isJsonNull }?.asString
|
||||
val sessionMsgCount = item.get("message_count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val matchedRole = item.get("matched_role")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val header = whenField?.let { "📅 $it" } ?: ""
|
||||
val sourceTag = source?.let { "[$it]" } ?: ""
|
||||
val titleLine = title?.let { "\n 📄 $it" } ?: ""
|
||||
val modelLine = model?.let { "\n 🤖 $it" } ?: ""
|
||||
val matchedLine = matchedRole?.let { "\n 🎯 matched: $it" } ?: ""
|
||||
val countLine = sessionMsgCount?.let { "\n $it msgs" } ?: ""
|
||||
val snippetText = snippet ?: preview
|
||||
val snippetLine =
|
||||
snippetText?.let {
|
||||
val clean = it.take(200).replace("\n", " ")
|
||||
"\n ┃ $clean"
|
||||
} ?: ""
|
||||
|
||||
"━━━ #${idx + 1} $header$sourceTag$titleLine$modelLine$matchedLine$countLine$snippetLine"
|
||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
||||
|
||||
// Format messages (scroll / read)
|
||||
val anchorId = dataSource.get("around_message_id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val formattedMessages =
|
||||
messagesArray?.mapNotNull { element ->
|
||||
if (!element.isJsonObject) return@mapNotNull null
|
||||
val item = element.asJsonObject
|
||||
val msgId = item.get("id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val role = item.get("role")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
||||
val content = item.get("content")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val toolName = item.get("tool_name")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val roleEmoji =
|
||||
when (role) {
|
||||
"user" -> "👤"
|
||||
"assistant" -> "🤖"
|
||||
"tool" -> "🔧"
|
||||
else -> "❓"
|
||||
}
|
||||
val namePart = if (toolName != null) " ($toolName)" else ""
|
||||
val anchor = if (anchorId != null && msgId == anchorId) " ⬅️" else ""
|
||||
val cleanContent = content.take(300).replace("\n", " ")
|
||||
|
||||
"[$msgId] $roleEmoji $role$namePart$anchor\n $cleanContent"
|
||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val ssMainOutput = formattedResults ?: formattedMessages
|
||||
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = ssSummaryText,
|
||||
mainOutput = ssMainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Memory-specific formatting ──
|
||||
if (resolvedToolName == "memory") {
|
||||
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val success = dataSource.get("success")?.takeIf { !it.isJsonNull }?.asBoolean ?: true
|
||||
val target = argsObj?.get("target")?.takeIf { !it.isJsonNull }?.asString
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
val usage = dataSource.get("usage")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val summaryText = "💾 $action${target?.let { " ($it)" } ?: ""}"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
!success -> "❌ Failed"
|
||||
usage != null -> "✅ $action in ${target ?: "memory"}\n $usage"
|
||||
else -> "✅ ${target?.capitalize() ?: "Memory"} $action done"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Cronjob-specific formatting ──
|
||||
if (resolvedToolName == "cronjob") {
|
||||
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val jobsArray = dataSource.get("jobs")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
val jobId = dataSource.get("job_id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val jobName = dataSource.get("name")?.takeIf { !it.isJsonNull }?.asString
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val summaryText = "🔄 $action${jobName?.let { ": $it" } ?: jobId?.let { " (ID: $it)" } ?: ""}"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
jobsArray != null -> {
|
||||
jobsArray.mapNotNull { el ->
|
||||
if (!el.isJsonObject) return@mapNotNull null
|
||||
val j = el.asJsonObject
|
||||
val name = j.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
||||
val schedule = j.get("schedule")?.takeIf { !it.isJsonNull }?.asString
|
||||
val status = j.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||
val lastRun = j.get("last_run")?.takeIf { !it.isJsonNull }?.asString
|
||||
val parts = mutableListOf("📌 $name")
|
||||
if (schedule != null) parts.add(" ⏱ $schedule")
|
||||
if (status != null) parts.add(" 📊 $status")
|
||||
if (lastRun != null) parts.add(" ⏮ $lastRun")
|
||||
parts.joinToString("\n")
|
||||
}.joinToString("\n\n")
|
||||
}
|
||||
jobId != null -> "✅ $action (ID: $jobId)"
|
||||
else -> "✅ $action done"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Web Search-specific formatting ──
|
||||
if (resolvedToolName == "web_search") {
|
||||
val query = argsObj?.get("query")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val success = dataSource.get("success")?.takeIf { !it.isJsonNull }?.asBoolean ?: true
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
val webArr =
|
||||
dataSource.get("data")?.takeIf { !it.isJsonNull && it.isJsonObject }
|
||||
?.asJsonObject?.get("web")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
|
||||
val summaryText = "🌐 $query${webArr?.let { " (${it.size()} results)" } ?: ""}"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
webArr != null ->
|
||||
webArr.mapNotNull { el ->
|
||||
if (!el.isJsonObject) return@mapNotNull null
|
||||
val item = el.asJsonObject
|
||||
val title = item.get("title")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val url = item.get("url")?.takeIf { !it.isJsonNull }?.asString
|
||||
val desc = item.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||
val pos = item.get("position")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val lines = mutableListOf("${pos?.let { "$it. " } ?: ""}$title")
|
||||
if (desc != null) lines.add(" $desc")
|
||||
if (url != null) lines.add(" 🔗 $url")
|
||||
lines.joinToString("\n")
|
||||
}.joinToString("\n\n")
|
||||
!success -> "❌ Search failed"
|
||||
else -> "No results"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Skills List-specific formatting ──
|
||||
if (resolvedToolName == "skills_list") {
|
||||
val category = argsObj?.get("category")?.takeIf { !it.isJsonNull }?.asString
|
||||
val skillsArr =
|
||||
(dataSource.get("skills") ?: dataSource.get("results"))
|
||||
?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
|
||||
val summaryText = "📚 ${skillsArr?.size() ?: 0} skills${category?.let { " ($it)" } ?: ""}"
|
||||
val mainOutput =
|
||||
skillsArr?.mapNotNull { el ->
|
||||
if (!el.isJsonObject) return@mapNotNull null
|
||||
val s = el.asJsonObject
|
||||
val name = s.get("name")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val desc = s.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||
val cat = s.get("category")?.takeIf { !it.isJsonNull }?.asString
|
||||
val lines = mutableListOf("📌 $name")
|
||||
if (desc != null) lines.add(" $desc")
|
||||
if (cat != null) lines.add(" [$cat]")
|
||||
lines.joinToString("\n")
|
||||
}?.joinToString("\n\n") ?: "📚 No skills found"
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Skill View-specific formatting ──
|
||||
if (resolvedToolName == "skill_view") {
|
||||
val name = argsObj?.get("name")?.takeIf { !it.isJsonNull }?.asString
|
||||
val content = dataSource.get("content")?.takeIf { !it.isJsonNull }?.asString
|
||||
val linkedFiles = dataSource.get("linked_files")?.takeIf { !it.isJsonNull && it.isJsonObject }?.asJsonObject
|
||||
|
||||
val summaryText = "📖 $name"
|
||||
val contentPreview =
|
||||
content?.let { c ->
|
||||
if (c.length > 500) c.take(500) + "\n... [${c.length - 500} more chars]" else c
|
||||
}
|
||||
val filesInfo =
|
||||
linkedFiles?.entrySet()?.joinToString("\n") { (k, v) ->
|
||||
val paths =
|
||||
when {
|
||||
v.isJsonArray -> v.asJsonArray.joinToString(", ") { it.asString }
|
||||
v.isJsonPrimitive -> v.asString
|
||||
else -> v.toString()
|
||||
}
|
||||
" 📎 $k: $paths"
|
||||
}
|
||||
val mainOutput =
|
||||
buildString {
|
||||
if (contentPreview != null) append(contentPreview)
|
||||
if (filesInfo != null) append("\n\n━━━ Linked Files ━━━\n$filesInfo")
|
||||
if (content == null && linkedFiles == null) append("📖 No content available")
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Process-specific formatting ──
|
||||
if (resolvedToolName == "process") {
|
||||
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val procId = argsObj?.get("session_id")?.takeIf { !it.isJsonNull }?.asString
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
val status = dataSource.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||
val outputText = dataSource.get("output")?.takeIf { !it.isJsonNull }?.asString
|
||||
val processesArr = dataSource.get("processes")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
|
||||
val summaryText = "⚙️ $action${procId?.let { ": $it" } ?: ""}"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
processesArr != null -> {
|
||||
processesArr.mapNotNull { el ->
|
||||
if (!el.isJsonObject) return@mapNotNull null
|
||||
val p = el.asJsonObject
|
||||
val pid =
|
||||
p.get(
|
||||
"session_id",
|
||||
)?.takeIf { !it.isJsonNull }?.asString ?: p.get("id")?.asString ?: "?"
|
||||
val pStatus = p.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||
val cmd = p.get("command")?.takeIf { !it.isJsonNull }?.asString
|
||||
val running = p.get("running")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||
val parts = mutableListOf("📌 $pid${cmd?.let { ": $it" } ?: ""}")
|
||||
if (pStatus != null) parts.add(" Status: $pStatus")
|
||||
if (running != null) parts.add(" Running: $running")
|
||||
parts.joinToString("\n")
|
||||
}.joinToString("\n\n")
|
||||
}
|
||||
outputText != null -> "${status?.let { "Status: $it\n" } ?: ""}$outputText"
|
||||
status != null -> "✅ $status"
|
||||
else -> "✅ $action done"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── X Search-specific formatting ──
|
||||
if (resolvedToolName == "x_search") {
|
||||
val query = argsObj?.get("query")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val answer = dataSource.get("answer")?.takeIf { !it.isJsonNull }?.asString
|
||||
val citationsArr = dataSource.get("citations")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
val degraded = dataSource.get("degraded")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val summaryText = "𝕏 $query${if (degraded == true) " (no citations)" else ""}"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
answer != null -> {
|
||||
val lines = mutableListOf(answer)
|
||||
if (citationsArr != null && citationsArr.size() > 0) {
|
||||
lines.add("\n━━━ Citations ━━━")
|
||||
citationsArr.forEachIndexed { idx, cit ->
|
||||
if (cit.isJsonObject) {
|
||||
val url = cit.asJsonObject.get("url")?.asString
|
||||
val title = cit.asJsonObject.get("title")?.asString
|
||||
lines.add("${idx + 1}. ${title ?: url ?: "source"}")
|
||||
if (title != null && url != null) lines.add(" $url")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (degraded == true) lines.add("\n⚠️ No citations — answer based on model's knowledge")
|
||||
lines.joinToString("\n")
|
||||
}
|
||||
else -> "𝕏 No results"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Vision Analyze-specific formatting ──
|
||||
if (resolvedToolName == "vision_analyze") {
|
||||
val imageUrl = argsObj?.get("image_url")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val description = dataSource.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||
val content = dataSource.get("content")?.takeIf { !it.isJsonNull }?.asString
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val summaryText = "👁️ ${imageUrl.take(60)}"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
description != null -> description
|
||||
content != null -> content
|
||||
else -> "👁️ No description available"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Web Extract-specific formatting ──
|
||||
if (resolvedToolName == "web_extract") {
|
||||
val urls = argsObj?.get("urls")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
val content = dataSource.get("content")?.takeIf { !it.isJsonNull }?.asString
|
||||
val success = dataSource.get("success")?.takeIf { !it.isJsonNull }?.asBoolean ?: true
|
||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||
|
||||
val urlSummary = urls?.joinToString(", ") { it.asString?.take(40) ?: "?" } ?: ""
|
||||
val summaryText = "🕸️ ${urls?.size() ?: 0} page(s)"
|
||||
val mainOutput =
|
||||
when {
|
||||
errorMsg != null -> "❌ $errorMsg"
|
||||
content != null ->
|
||||
content.take(
|
||||
3000,
|
||||
) + if (content.length > 3000) "\n... [${content.length - 3000} more chars]" else ""
|
||||
else -> "🕸️ No content extracted"
|
||||
}
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Tool Search-specific formatting ──
|
||||
if (resolvedToolName == "tool_search") {
|
||||
val query = argsObj?.get("query")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||
val limit = argsObj?.get("limit")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
||||
val matchesArr = dataSource.get("matches")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||
|
||||
val summaryText = "🔍 $query${matchesArr?.let { " (${it.size()} matches)" } ?: ""}"
|
||||
val mainOutput =
|
||||
matchesArr?.mapNotNull { el ->
|
||||
if (!el.isJsonObject) return@mapNotNull null
|
||||
val m = el.asJsonObject
|
||||
val name = m.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
||||
val desc = m.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||
val lines = mutableListOf("🔧 $name")
|
||||
if (desc != null) lines.add(" $desc")
|
||||
lines.joinToString("\n")
|
||||
}?.joinToString("\n\n") ?: "🔍 No matching tools"
|
||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||
return ParsedToolData(
|
||||
toolName = resolvedToolName ?: "",
|
||||
args = args,
|
||||
result = dataSource.entrySet().associate { it.key to it.value.toString() },
|
||||
summaryText = summaryText,
|
||||
mainOutput = mainOutput,
|
||||
durationSec = duration,
|
||||
isRunning = isRunning,
|
||||
)
|
||||
}
|
||||
|
||||
// Check if this looks like a terminal result (check dataSource for backward compat)
|
||||
val hasStdout = dataSource.has("stdout")
|
||||
val hasStderr = dataSource.has("stderr")
|
||||
|
||||
@@ -104,6 +104,163 @@ object ToolSchemaRegistry {
|
||||
summaryPrefix = "▶️ ",
|
||||
iconEmoji = "▶️",
|
||||
),
|
||||
"todo" to
|
||||
ToolDisplayConfig(
|
||||
name = "todo",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "📋",
|
||||
),
|
||||
"fact_store" to
|
||||
ToolDisplayConfig(
|
||||
name = "fact_store",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "🧠",
|
||||
),
|
||||
"session_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "session_search",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "🔍",
|
||||
),
|
||||
// ── Action-based tools ──
|
||||
"cronjob" to
|
||||
ToolDisplayConfig(
|
||||
name = "cronjob",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "🔄",
|
||||
),
|
||||
"memory" to
|
||||
ToolDisplayConfig(
|
||||
name = "memory",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "💾",
|
||||
),
|
||||
"fact_feedback" to
|
||||
ToolDisplayConfig(
|
||||
name = "fact_feedback",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "👍",
|
||||
),
|
||||
"process" to
|
||||
ToolDisplayConfig(
|
||||
name = "process",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "⚙️",
|
||||
),
|
||||
"skill_manage" to
|
||||
ToolDisplayConfig(
|
||||
name = "skill_manage",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "🛠️",
|
||||
),
|
||||
// ── Web / Browser tools ──
|
||||
"web_extract" to
|
||||
ToolDisplayConfig(
|
||||
name = "web_extract",
|
||||
summaryArgKey = "urls",
|
||||
iconEmoji = "🕸️",
|
||||
),
|
||||
"browser_type" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_type",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "⌨️",
|
||||
),
|
||||
"browser_cdp" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_cdp",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "🔧",
|
||||
),
|
||||
"browser_dialog" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_dialog",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "💬",
|
||||
),
|
||||
// ── Media / Vision tools ──
|
||||
"vision_analyze" to
|
||||
ToolDisplayConfig(
|
||||
name = "vision_analyze",
|
||||
summaryArgKey = "image_url",
|
||||
iconEmoji = "👁️",
|
||||
),
|
||||
"text_to_speech" to
|
||||
ToolDisplayConfig(
|
||||
name = "text_to_speech",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "🔊",
|
||||
),
|
||||
"video_generate" to
|
||||
ToolDisplayConfig(
|
||||
name = "video_generate",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "🎬",
|
||||
),
|
||||
"image_generate" to
|
||||
ToolDisplayConfig(
|
||||
name = "image_generate",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "🎨",
|
||||
),
|
||||
// ── Social / Messaging tools ──
|
||||
"x_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "x_search",
|
||||
summaryArgKey = "query",
|
||||
iconEmoji = "𝕏",
|
||||
),
|
||||
"send_message" to
|
||||
ToolDisplayConfig(
|
||||
name = "send_message",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "📨",
|
||||
),
|
||||
// ── Skills tools ──
|
||||
"skills_list" to
|
||||
ToolDisplayConfig(
|
||||
name = "skills_list",
|
||||
summaryArgKey = null,
|
||||
iconEmoji = "📚",
|
||||
),
|
||||
"skill_view" to
|
||||
ToolDisplayConfig(
|
||||
name = "skill_view",
|
||||
summaryArgKey = "name",
|
||||
iconEmoji = "📖",
|
||||
),
|
||||
// ── On-demand tools ──
|
||||
"tool_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "tool_search",
|
||||
summaryArgKey = "query",
|
||||
iconEmoji = "🔍",
|
||||
),
|
||||
"tool_describe" to
|
||||
ToolDisplayConfig(
|
||||
name = "tool_describe",
|
||||
summaryArgKey = "name",
|
||||
iconEmoji = "📖",
|
||||
),
|
||||
"tool_call" to
|
||||
ToolDisplayConfig(
|
||||
name = "tool_call",
|
||||
summaryArgKey = "name",
|
||||
iconEmoji = "🔧",
|
||||
),
|
||||
// ── Misc ──
|
||||
"read_terminal" to
|
||||
ToolDisplayConfig(
|
||||
name = "read_terminal",
|
||||
summaryArgKey = "session_id",
|
||||
iconEmoji = "🖥️",
|
||||
),
|
||||
"computer_use" to
|
||||
ToolDisplayConfig(
|
||||
name = "computer_use",
|
||||
summaryArgKey = "action",
|
||||
iconEmoji = "🖥️",
|
||||
),
|
||||
)
|
||||
|
||||
fun getDisplayConfig(toolName: String?): ToolDisplayConfig =
|
||||
|
||||
Reference in new issue
Block a user