Chuyển tới nội dung chính

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_request block
  • Phản hồi bằng answers thay vì decisions
  • History: trả approval_request block với isResolved: true trong 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
}
]
}
FieldTypeMô tả
questionstringNội dung câu hỏi
headerstring?Tiêu đề nhóm câu hỏi
multiSelectbooleantrue = chọn nhiều, false = chọn một
optionsarrayDanh sách lựa chọn
options[].labelstringText hiển thị
options[].descriptionstring?Mô tả thêm
options[].inputboolean?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"
}
}
FieldMô tả
answersDùng cho ask_user_question{question: answer}
decisionsDù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_calls array (đã convert thành content block)
  • Frontend parse approval_request block → render ResolvedQuestionBlock (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.