mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-17 21:12:13 +00:00
fix: external agent tool call argument handling for WebFetch
This commit is contained in:
parent
6a53318fea
commit
12776e5f47
5 changed files with 126 additions and 19 deletions
|
|
@ -495,7 +495,9 @@ class ExternalAcpAgentService(private val project: Project) {
|
|||
private fun handleToolCall(update: AcpSessionUpdate.ToolCall, events: AgentEvents) {
|
||||
val toolCall = update.toolCall
|
||||
toolCallsById[toolCall.id] = ExternalToolCall(toolCall.toolName, toolCall.args)
|
||||
events.onToolStarting(toolCall.id, toolCall.toolName, toolCall.args)
|
||||
if (!shouldDeferToolStart(toolCall.toolName, toolCall.args, update.status)) {
|
||||
events.onToolStarting(toolCall.id, toolCall.toolName, toolCall.args)
|
||||
}
|
||||
|
||||
if (update.status?.isTerminal == true) {
|
||||
completeToolCall(
|
||||
|
|
@ -513,15 +515,28 @@ class ExternalAcpAgentService(private val project: Project) {
|
|||
update: AcpSessionUpdate.ToolCallUpdate,
|
||||
events: AgentEvents
|
||||
) {
|
||||
val updatedToolCall = update.toolCall
|
||||
val currentToolCall = toolCallsById[update.toolCallId]
|
||||
val effectiveToolName = updatedToolCall?.toolName ?: currentToolCall?.toolName ?: "Tool"
|
||||
val effectiveArgs = updatedToolCall?.args ?: currentToolCall?.args
|
||||
|
||||
if (updatedToolCall != null) {
|
||||
toolCallsById[update.toolCallId] =
|
||||
ExternalToolCall(effectiveToolName, effectiveArgs)
|
||||
}
|
||||
|
||||
if (currentToolCall?.args == null && effectiveArgs != null) {
|
||||
events.onToolStarting(update.toolCallId, effectiveToolName, effectiveArgs)
|
||||
}
|
||||
|
||||
if (!update.status.isTerminal) {
|
||||
return
|
||||
}
|
||||
|
||||
val toolCall = toolCallsById[update.toolCallId]
|
||||
completeToolCall(
|
||||
toolCallId = update.toolCallId,
|
||||
toolName = toolCall?.toolName ?: "Tool",
|
||||
args = toolCall?.args,
|
||||
toolName = effectiveToolName,
|
||||
args = effectiveArgs,
|
||||
status = update.status,
|
||||
rawOutput = update.rawOutput,
|
||||
events = events
|
||||
|
|
@ -546,6 +561,14 @@ class ExternalAcpAgentService(private val project: Project) {
|
|||
events.onToolCompleted(toolCallId, toolName, result)
|
||||
}
|
||||
|
||||
private fun shouldDeferToolStart(
|
||||
toolName: String,
|
||||
args: Any?,
|
||||
status: AcpToolCallStatus?
|
||||
): Boolean {
|
||||
return toolName == "WebFetch" && args == null && status?.isTerminal != true
|
||||
}
|
||||
|
||||
private fun handleConfigOptionUpdate(update: JsonObject) {
|
||||
project.service<AgentToolWindowContentManager>()
|
||||
.getSession(proxySessionId)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ internal sealed interface AcpSessionUpdate {
|
|||
|
||||
data class ToolCallUpdate(
|
||||
val toolCallId: String,
|
||||
val toolCall: AcpDecodedToolCall?,
|
||||
val status: AcpToolCallStatus,
|
||||
val rawOutput: JsonElement?
|
||||
) : AcpSessionUpdate
|
||||
|
|
@ -22,12 +23,13 @@ internal sealed interface AcpSessionUpdate {
|
|||
}
|
||||
|
||||
internal enum class AcpToolCallStatus(val wireValue: String) {
|
||||
IN_PROGRESS("in_progress"),
|
||||
COMPLETED("completed"),
|
||||
FAILED("failed"),
|
||||
CANCELLED("cancelled");
|
||||
|
||||
val isTerminal: Boolean
|
||||
get() = true
|
||||
get() = this != IN_PROGRESS
|
||||
|
||||
companion object {
|
||||
fun fromWireValue(value: String?): AcpToolCallStatus? {
|
||||
|
|
@ -84,6 +86,7 @@ internal class AcpSessionUpdateParser(
|
|||
val status = AcpToolCallStatus.fromWireValue(update.string("status")) ?: return null
|
||||
return AcpSessionUpdate.ToolCallUpdate(
|
||||
toolCallId = toolCallId,
|
||||
toolCall = toolCallDecoder.decodeToolCall(update),
|
||||
status = status,
|
||||
rawOutput = update["rawOutput"] ?: update["content"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ internal class AcpToolCallDecoder(
|
|||
"Read" -> decodeReadArgs(obj, metadata) ?: payload.ifBlank { null }
|
||||
"IntelliJSearch" -> decodeSearchArgs(obj) ?: payload.ifBlank { null }
|
||||
"Bash" -> decodeBashArgs(obj) ?: payload.ifBlank { null }
|
||||
"WebFetch" -> decodeWebFetchArgs(obj, rawInput, metadata) ?: payload.ifBlank { null }
|
||||
else -> payload.ifBlank { null }
|
||||
}
|
||||
}
|
||||
|
|
@ -291,6 +292,27 @@ internal class AcpToolCallDecoder(
|
|||
)
|
||||
}
|
||||
|
||||
private fun decodeWebFetchArgs(
|
||||
obj: JsonObject,
|
||||
rawInput: JsonElement?,
|
||||
metadata: JsonObject? = null
|
||||
): WebFetchTool.Args? {
|
||||
val payload = rawInput.toPayloadString()
|
||||
val url = obj.string("url", "uri", "href", "link")
|
||||
?: metadata?.string("url", "uri")
|
||||
?: extractFirstUrl(payload)
|
||||
?: metadata?.string("title")?.let(::extractFirstUrl)
|
||||
?: return null
|
||||
|
||||
return WebFetchTool.Args(
|
||||
url = url,
|
||||
selector = obj.string("selector", "css_selector", "cssSelector"),
|
||||
timeoutMs = obj.int("timeout_ms", "timeoutMs", "timeout") ?: 10_000,
|
||||
offset = obj.int("offset", "start_line", "startLine"),
|
||||
limit = obj.int("limit", "max_lines", "maxLines", "count")
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeMcpArgs(rawTitle: String, rawInput: JsonElement?): McpTool.Args {
|
||||
val obj = rawInput.asJsonObjectOrNull(json) ?: JsonObject(emptyMap())
|
||||
val callName = rawTitle.ifBlank { obj.string("tool_name", "toolName") ?: "unknown" }
|
||||
|
|
@ -333,4 +355,12 @@ internal class AcpToolCallDecoder(
|
|||
val slashIndex = candidate.indexOf('/')
|
||||
return slashIndex > 0 && slashIndex < candidate.length - 1
|
||||
}
|
||||
|
||||
private fun extractFirstUrl(text: String): String? {
|
||||
return URL_REGEX.find(text)?.value
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val URL_REGEX = Regex("""https?://[^\s"'<>]+""")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -645,7 +645,10 @@ class CustomServiceForm(
|
|||
|
||||
private fun createTokenSpinner(initialValue: Long): JSpinner =
|
||||
JSpinner(SpinnerNumberModel(initialValue, 1L, Long.MAX_VALUE, 1L)).apply {
|
||||
(editor as? JSpinner.DefaultEditor)?.textField?.columns = 10
|
||||
(editor as? JSpinner.DefaultEditor)?.textField?.apply {
|
||||
columns = 10
|
||||
font = nameField.font
|
||||
}
|
||||
}
|
||||
|
||||
private fun readSpinnerValue(spinner: JSpinner, fallback: Long): Long {
|
||||
|
|
|
|||
|
|
@ -528,21 +528,12 @@ object ToolCallDescriptorFactory {
|
|||
result: Any?,
|
||||
projectId: String?
|
||||
): ToolCallDescriptor {
|
||||
val query = when (args) {
|
||||
is WebSearchTool.Args -> args.query
|
||||
is WebFetchTool.Args -> args.url
|
||||
else -> "Unknown"
|
||||
}
|
||||
val isFetch = isWebFetchArgs(args) || result is WebFetchTool.Result
|
||||
val query = extractWebQuery(args, result)
|
||||
|
||||
val titlePrefix = when (args) {
|
||||
is WebFetchTool.Args -> "Fetch:"
|
||||
else -> "Web:"
|
||||
}
|
||||
val titlePrefix = if (isFetch) "Fetch:" else "Web:"
|
||||
|
||||
val tooltip = when (args) {
|
||||
is WebFetchTool.Args -> "Fetch: $query"
|
||||
else -> "Web search: $query"
|
||||
}
|
||||
val tooltip = if (isFetch) "Fetch: $query" else "Web search: $query"
|
||||
|
||||
val truncatedQuery = truncateQuery(query)
|
||||
|
||||
|
|
@ -559,6 +550,45 @@ object ToolCallDescriptorFactory {
|
|||
)
|
||||
}
|
||||
|
||||
private fun extractWebQuery(args: Any, result: Any?): String {
|
||||
return when (args) {
|
||||
is WebSearchTool.Args -> args.query
|
||||
is WebFetchTool.Args -> args.url
|
||||
is JsonObject -> jsonObjectString(
|
||||
args,
|
||||
"url",
|
||||
"uri",
|
||||
"href",
|
||||
"link",
|
||||
"query",
|
||||
"q"
|
||||
) ?: extractFirstUrl(args.toString()) ?: "Unknown"
|
||||
|
||||
is Map<*, *> -> mapString(
|
||||
args,
|
||||
"url",
|
||||
"uri",
|
||||
"href",
|
||||
"link",
|
||||
"query",
|
||||
"q"
|
||||
) ?: extractFirstUrl(args.toString()) ?: "Unknown"
|
||||
|
||||
is String -> extractFirstUrl(args) ?: args.takeIf { it.isNotBlank() } ?: "Unknown"
|
||||
else -> (result as? WebFetchTool.Result)?.url ?: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWebFetchArgs(args: Any): Boolean {
|
||||
return when (args) {
|
||||
is WebFetchTool.Args -> true
|
||||
is JsonObject -> jsonObjectString(args, "url", "uri", "href", "link") != null
|
||||
is Map<*, *> -> mapString(args, "url", "uri", "href", "link") != null
|
||||
is String -> extractFirstUrl(args) != null
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTaskDescriptor(
|
||||
args: Any,
|
||||
result: Any?,
|
||||
|
|
@ -810,6 +840,24 @@ object ToolCallDescriptorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private fun jsonObjectString(obj: JsonObject, vararg keys: String): String? {
|
||||
return keys.firstNotNullOfOrNull { key ->
|
||||
(obj[key] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapString(map: Map<*, *>, vararg keys: String): String? {
|
||||
return keys.firstNotNullOfOrNull { key ->
|
||||
map[key]?.toString()?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractFirstUrl(text: String): String? {
|
||||
return URL_REGEX.find(text)?.value
|
||||
}
|
||||
|
||||
private val URL_REGEX = Regex("""https?://[^\s"'<>]+""")
|
||||
|
||||
private fun truncateCommand(command: String): String {
|
||||
return if (command.length > AgentUiConfig.BASH_CMD_MAX) {
|
||||
command.take(AgentUiConfig.BASH_CMD_MAX) + "..."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue