Luồng Hỏi Người Dùng (Ask User Question)
Tổng Quan
ask_user_question là tool đặc biệt — agent dừng lại để hỏi người dùng. Khác với tool thông thường:
- Tách khỏi group — close group trước, hiển thị question UI ngoài group
- Không stream tool_use/tool_result — chỉ stream
approval_requestblock - Phản hồi bằng
answersthay vìdecisions - History: trả
approval_requestblock vớiisResolved: truetrong content
Stream Flow
Server Client
│ │
│── group_end ────────────────────────────▶│ ← close group trước
│ │
│── content_block_start ▶│
│ (type: approval_request) │
│── content_block_delta ▶│
│ (action_requests: ask_user_question) │
│── content_block_stop ───────────────────▶│
│ │
│ (agent đang chờ...) │ ← hiển thị question UI
│ │
│◀── approval { answers: {...} } ──────────│
│ │
│── group_start ──────────────────────────▶│ ← group mới cho tool calls tiếp
│── (agent tiếp tục...) ▶│
Ví dụ stream events
text (share_thought) → standalone content
group_start → group mở
tool_use: write_todos
tool_result: write_todos
group_end → group đóng
← ask_user_question NGOÀI group
content_block_start (approval_request)
content_block_delta (questions + options)
content_block_stop
← user chọn option
group_start → group MỚI
tool_use: analyze_price
...
Block approval_request — Stream
Start:
{
"type": "content_block_start",
"index": 3,
"content_block": {
"type": "approval_request",
"approval_key": "session-id_1"
}
}
Delta:
{
"type": "content_block_delta",
"index": 3,
"delta": {
"action_requests": [
{
"name": "ask_user_question",
"args": {
"questions": [
{
"question": "Thảo muốn tập trung vào mục tiêu nào?",
"header": "Mục tiêu chính",
"multiSelect": false,
"options": [
{
"label": "Cổ tức bền vững (Recommended)",
"description": "Tập trung cổ phiếu trả cổ tức đều"
},
{
"label": "Tăng trưởng dài hạn",
"description": "Lợi nhuận từ giá tăng trưởng"
},
{
"label": "Khác",
"description": "Nhập giá trị tùy chỉnh",
"input": true
}
]
},
{
"question": "Thời gian nắm giữ dự kiến?",
"header": "Kỳ hạn đầu tư",
"multiSelect": false,
"options": [
{
"label": "Trên 3 năm",
"description": "Tích lũy dài hạn"
},
{
"label": "1-3 năm",
"description": "Theo chu kỳ ngành"
},
{
"label": "Khác",
"description": "Nhập giá trị tùy chỉnh",
"input": true
}
]
}
]
}
}
],
"review_configs": [
{
"action_name": "ask_user_question",
"allowed_decisions": [
"approve",
"edit",
"reject"
]
}
],
"timeout_seconds": 600
}
}
Stop:
{
"type": "content_block_stop",
"index": 3
}
Cấu Trúc Câu Hỏi
Mỗi phần tử trong questions:
{
"question": "Câu hỏi hiển thị cho user",
"header": "Tiêu đề nhóm (optional)",
"multiSelect": false,
"options": [
{
"label": "Option 1",
"description": "Mô tả chi tiết"
},
{
"label": "Option 2 (Recommended)",
"description": "Được đề xuất"
},
{
"label": "Khác",
"description": "Nhập giá trị tùy chỉnh",
"input": true
}
]
}
| Field | Type | Mô tả |
|---|---|---|
question | string | Nội dung câu hỏi |
header | string? | Tiêu đề nhóm câu hỏi |
multiSelect | boolean | true = chọn nhiều, false = chọn một |
options | array | Danh sách lựa chọn |
options[].label | string | Text hiển thị |
options[].description | string? | Mô tả thêm |
options[].input | boolean? | true = option "Khác" cho phép nhập tự do |
Option "Khác" (Custom Input)
Backend tự thêm option {"label": "Khác", "input": true} nếu chưa có. Frontend hiển thị input text cho option này.
Gửi Trả Lời — approval message
Quan trọng: Dùng field answers (key = question text, value = answer text).
{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"answers": {
"Thảo muốn tập trung vào mục tiêu nào?": "Cổ tức bền vững (Recommended)",
"Thời gian nắm giữ dự kiến?": "Trên 3 năm"
}
}
| Field | Mô tả |
|---|---|
answers | Dùng cho ask_user_question — {question: answer} |
decisions | Dùng cho phê duyệt tool thông thường — KHÔNG dùng cho question |
Multi-select
Nhiều lựa chọn nối bằng , :
{
"answers": {
"Nhóm ngành quan tâm?": "Ngân hàng, Thép (Steel)"
}
}
Skip / Bỏ qua
User bỏ qua câu hỏi → gửi "[No preference]":
{
"answers": {
"Câu hỏi bị bỏ qua?": "[No preference]"
}
}
History API — Resolved Question Block
Sau khi user trả lời, history trả approval_request block với isResolved: true trong assistant message content:
{
"role": "assistant",
"message_type": "step",
"content": [
{
"type": "approval_request",
"isResolved": true,
"actionRequests": [
{
"name": "ask_user_question",
"args": {
"questions": [
{
"question": "Thảo muốn tập trung vào mục tiêu nào?",
"header": "Mục tiêu chính",
"multiSelect": false,
"options": [
{
"label": "Cổ tức bền vững (Recommended)",
"description": "..."
},
{
"label": "Tăng trưởng dài hạn",
"description": "..."
},
{
"label": "Khác",
"description": "Nhập giá trị tùy chỉnh",
"input": true
}
]
},
{
"question": "Thời gian nắm giữ dự kiến?",
"header": "Kỳ hạn đầu tư",
"multiSelect": false,
"options": [
{
"label": "Trên 3 năm",
"description": "..."
},
{
"label": "1-3 năm",
"description": "..."
},
{
"label": "Khác",
"description": "Nhập giá trị tùy chỉnh",
"input": true
}
]
}
],
"answers": {
"Thảo muốn tập trung vào mục tiêu nào?": "Cổ tức bền vững (Recommended)",
"Thời gian nắm giữ dự kiến?": "Trên 3 năm"
}
}
}
],
"submittedAnswers": {
"Thảo muốn tập trung vào mục tiêu nào?": "Cổ tức bền vững (Recommended)",
"Thời gian nắm giữ dự kiến?": "Trên 3 năm"
}
}
],
"display_type": "content"
}
Lưu ý History
display_type: "content"— hiển thị ngoài group (không collapsible)- Tool result message bị filter — không trả tool message
name: "ask_user_question"(data đã có trong approval_request block) - Tool call bị filter — không có trong
tool_callsarray (đã convert thành content block) - Frontend parse
approval_requestblock → renderResolvedQuestionBlock(hiển thị Q&A đã trả lời)
Ví dụ History đầy đủ — 2 lượt ask_user_question
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "hãy hỏi 2 lần"
}
],
"display_type": "content"
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "..."
}
],
"display_type": "group_start",
"summary": "Chuẩn bị câu hỏi lần 1"
},
{
"role": "assistant",
"content": [],
"tool_calls": [
{
"id": "tc-1",
"name": "write_todos",
"tool_content_message": "Chuẩn bị câu hỏi lần 1"
}
],
"display_type": "group_item"
},
{
"role": "tool",
"tool_call_id": "tc-1",
"name": "write_todos",
"status": "success",
"display_type": "group_end",
"summary": "Chuẩn bị câu hỏi lần 1"
},
{
"role": "assistant",
"content": [
{
"type": "approval_request",
"isResolved": true,
"actionRequests": [
{
"name": "ask_user_question",
"args": {
"questions": [
"..."
],
"answers": {
"Q1": "A1"
}
}
}
],
"submittedAnswers": {
"Q1": "A1"
}
}
],
"display_type": "content"
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "..."
}
],
"display_type": "group_start",
"summary": "Chuẩn bị câu hỏi lần 2"
},
{
"role": "assistant",
"content": [],
"tool_calls": [
{
"id": "tc-2",
"name": "write_todos",
"tool_content_message": "Chuẩn bị câu hỏi lần 2"
}
],
"display_type": "group_item"
},
{
"role": "tool",
"tool_call_id": "tc-2",
"name": "write_todos",
"status": "success",
"display_type": "group_end",
"summary": "Chuẩn bị câu hỏi lần 2"
},
{
"role": "assistant",
"content": [
{
"type": "approval_request",
"isResolved": true,
"actionRequests": [
{
"name": "ask_user_question",
"args": {
"questions": [
"..."
],
"answers": {
"Q2": "A2"
}
}
}
],
"submittedAnswers": {
"Q2": "A2"
}
}
],
"display_type": "content"
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Cảm ơn! Tổng kết...",
"is_final": true
}
],
"display_type": "content"
}
]
Render:
Chuẩn bị câu hỏi lần 1 > ← group collapsed
┌─────────────────────────────┐
│ Q1: ... │ ← resolved question (ngoài group)
│ A1: ... │
└─────────────────────────────┘
Chuẩn bị câu hỏi lần 2 > ← group collapsed
┌─────────────────────────────┐
│ Q2: ... │ ← resolved question (ngoài group)
│ A2: ... │
└─────────────────────────────┘
Cảm ơn! Tổng kết... ← final text
Frontend Implementation
Phân biệt Ask Question vs Approval thông thường
const isQuestion = actionRequests.some(a => a.name === 'ask_user_question');
if (isQuestion) {
// Hiển thị question UI (ngoài group)
// Gửi với field "answers"
} else {
// Hiển thị UI approve/reject (trong group)
// Gửi với field "decisions"
}
Parse history — approval_request block
Frontend parser (converters.js, ChatApp.jsx) cần handle approval_request type trong content blocks:
function parseAssistantContent(msg) {
const blocks = [];
for (const block of msg.content) {
if (block.type === 'thinking') { /* ... */ }
else if (block.type === 'text') { /* ... */ }
else if (block.type === 'tool_use') { /* ... */ }
else if (block.type === 'approval_request') {
blocks.push(block); // pass-through nguyên block
}
}
return blocks;
}
Render resolved question
function renderBlock(block, index, options) {
switch (block.type) {
case 'approval_request': {
const isQuestion = block.actionRequests?.some(a => a.name === 'ask_user_question');
if (block.isResolved && isQuestion) {
return <ResolvedQuestionBlock block={block} />;
}
// ... approval UI cho tool thông thường
}
}
}
function ResolvedQuestionBlock({ block }) {
const questionAction = block.actionRequests?.find(a => a.name === 'ask_user_question');
const questions = questionAction?.args?.questions || [];
const answers = block.submittedAnswers || {};
return (
<div className="border rounded-xl px-4 py-3 space-y-2">
{questions.map((q, idx) => (
<div key={idx}>
<div className="font-semibold">{q.question}</div>
<div className="text-gray-500">{answers[q.question] || '[No preference]'}</div>
</div>
))}
</div>
);
}
Backend Config — display_as_content
ask_user_question được config trong tool_stream_config.py:
"ask_user_question": ToolStreamRule(
stream_tool_use=False, # không stream tool_use block (approval_request thay thế)
stream_tool_result=False, # không stream tool_result block (data trong approval_request)
display_as_content=True, # tách khỏi group, hiển thị ngoài group
),
Các tool khác muốn tách group tương tự chỉ cần thêm display_as_content=True.
Timeout
Timeout mặc định: 600 giây (10 phút), dài hơn tool thông thường (300 giây).
Khi hết timeout, agent tự reject và emit approval_timeout block.