Current section
Files
Jump to
Current section
Files
lib/cmdc/tool/ask_user.ex
defmodule CMDC.Tool.AskUser do
@moduledoc """
向用户提问,等待用户回答后继续,对标 DeepAgents `ask_user` 工具。
## 执行流程
1. 生成唯一 `ref`
2. 广播 `{:ask_user, session_id, question, options, ref}` 事件
3. 阻塞等待 `{:cmdc_event, session_id, {:user_responded, session_id, ref, response}}` 消息
4. 超时(默认 5 分钟)则返回错误
5. 将用户回答作为工具结果返回 LLM
## 选项参数(可选)
`options` 字段支持预设选项列表,UI 可展示为单选/多选。
为空列表时视为自由文本输入。
## 注意
此工具会**阻塞当前工具执行 Task**,直到用户响应或超时。
外部需通过 `CMDC.Agent.respond_to_ask/3` 或发送对应事件来解除阻塞。
"""
@behaviour CMDC.Tool
@default_timeout 300_000
@impl true
def name, do: "ask_user"
@impl true
def description do
"Ask the user a question and wait for their response before continuing. " <>
"Use this when you need clarification, a decision, or information " <>
"that cannot be inferred from context. " <>
"Do NOT use this for simple yes/no decisions you can make yourself."
end
@impl true
def meta(%{"question" => q}), do: "Ask: #{String.slice(q, 0, 54)}"
def meta(_), do: "AskUser"
@impl true
def parameters do
%{
"type" => "object",
"properties" => %{
"question" => %{
"type" => "string",
"description" => "The question to ask the user. Be specific and concise."
},
"options" => %{
"type" => "array",
"items" => %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "string", "description" => "Option identifier"},
"label" => %{"type" => "string", "description" => "Display text for the option"}
},
"required" => ["id", "label"]
},
"description" =>
"Optional list of pre-defined answer options. " <>
"If provided, user selects from these choices. " <>
"If empty, free-form text input is expected."
}
},
"required" => ["question"]
}
end
@impl true
def execute(%{"question" => question} = args, context) do
session_id = context.session_id
options = Map.get(args, "options", [])
ref = generate_ref()
timeout = @default_timeout
CMDC.EventBus.subscribe(session_id)
CMDC.EventBus.broadcast(session_id, {:ask_user, session_id, question, options, ref})
result = wait_for_response(session_id, ref, timeout)
CMDC.EventBus.unsubscribe(session_id)
result
end
def execute(_args, _ctx), do: {:error, "Missing required parameter: question"}
# ==========================================================================
# 私有辅助
# ==========================================================================
defp wait_for_response(session_id, ref, timeout) do
receive do
{:cmdc_event, ^session_id, {:user_responded, ^session_id, ^ref, response}} ->
{:ok, format_response(response)}
{:cmdc_event, ^session_id, {:user_responded, ^session_id, ^ref, response, _meta}} ->
{:ok, format_response(response)}
after
timeout ->
{:error, "User did not respond within #{div(timeout, 1000)}s timeout"}
end
end
defp format_response(response) when is_binary(response), do: response
defp format_response(response) when is_list(response), do: Enum.join(response, ", ")
defp format_response(response), do: inspect(response)
defp generate_ref do
:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
end
end