PhoneWork/docs/claude/test_can_use_tool_ask.py
Yuyao Huang 72ebf3b75d feat(question): implement AskUserQuestion tool support
- Add question card builder and answer handling in feishu.py
- Extend SDKSession with pending question state and answer method
- Update card callback handler to support question answers
- Add test cases for question flow and card responses
- Document usage with test_can_use_tool_ask.py example
2026-04-02 08:52:50 +08:00

161 lines
5.4 KiB
Python

"""Example: can_use_tool callback — intercept AskUserQuestion and provide answers.
Demonstrates:
- can_use_tool callback for tool permission control
- Detecting AskUserQuestion tool calls
- Using PermissionResultAllow(updated_input=...) to pre-fill user answers
- Auto-allowing read-only tools
This pattern is used by the secretary model to forward CC questions
to Feishu users and relay their responses back.
Usage:
cd docs/claude
../../.venv/Scripts/python test_can_use_tool_ask.py
"""
from __future__ import annotations
import asyncio
import json
from _sdk_test_common import auth_env, make_tmpdir, remove_tmpdir, setup_auth
# Simulated user responses (in production, these come from Feishu card callbacks)
SIMULATED_ANSWERS: dict[str, str] = {}
permission_log: list[dict] = []
async def permission_callback(tool_name, input_data, context):
"""can_use_tool callback that intercepts AskUserQuestion.
For AskUserQuestion:
- Extracts questions and options from input_data
- Provides pre-filled answers via updated_input
- Returns PermissionResultAllow with the modified input
For other tools:
- Read-only tools: auto-allow
- Write tools: auto-allow (for testing)
"""
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
permission_log.append({
"tool": tool_name,
"input_keys": list(input_data.keys()),
})
if tool_name == "AskUserQuestion":
questions = input_data.get("questions", [])
print(f"\n 🔔 AskUserQuestion intercepted! ({len(questions)} questions)")
# Build answers dict: {question_text: selected_option_label}
answers = {}
for q in questions:
question_text = q.get("question", "")
options = q.get("options", [])
multi = q.get("multiSelect", False)
print(f" Q: {question_text}")
for opt in options:
print(f" - {opt['label']}: {opt.get('description', '')[:60]}")
# In production: send card to Feishu, wait for user selection
# Here: use first option as simulated answer
if options:
selected = SIMULATED_ANSWERS.get(question_text, options[0]["label"])
answers[question_text] = selected
print(f" → Selected: {selected}")
# Pre-fill answers in the tool input via updated_input
modified_input = dict(input_data)
if "answers" not in modified_input:
modified_input["answers"] = {}
if isinstance(modified_input["answers"], dict):
modified_input["answers"].update(answers)
print(f" → updated_input.answers = {answers}")
return PermissionResultAllow(updated_input=modified_input)
# Auto-allow everything else for this test
return PermissionResultAllow()
async def main() -> int:
ok, msg = setup_auth()
print(msg)
if not ok:
return 1
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
SystemMessage,
TextBlock,
ToolUseBlock,
)
tmpdir = make_tmpdir("ask-test-")
try:
print("--- Test: can_use_tool intercepts AskUserQuestion ---")
print(f" tmpdir: {tmpdir}")
opts = ClaudeAgentOptions(
cwd=str(tmpdir),
permission_mode="default",
can_use_tool=permission_callback,
max_turns=5,
env=auth_env(),
)
# Ask Claude to ask the user a question — this will trigger AskUserQuestion
prompt = (
"I need you to ask the user a question using the AskUserQuestion tool. "
"Ask them: 'Which programming language do you prefer?' "
"with options: 'Python' (great for AI), 'TypeScript' (great for web). "
"Then tell me what they chose."
)
print(f"\n Prompt: {prompt[:100]}...")
async with ClaudeSDKClient(opts) as client:
await client.query(prompt)
ask_seen = False
result_text = ""
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f" Claude: {block.text[:200]}")
elif isinstance(block, ToolUseBlock):
if block.name == "AskUserQuestion":
ask_seen = True
print(f" [ToolUse] AskUserQuestion called!")
else:
print(f" [ToolUse] {block.name}")
elif isinstance(msg, ResultMessage):
result_text = msg.result or ""
print(f"\n Result: {result_text[:300]}")
print(f"\n Permission log ({len(permission_log)} entries):")
for entry in permission_log:
print(f" {entry['tool']}: keys={entry['input_keys']}")
if ask_seen:
print("\n ✅ PASS: AskUserQuestion was intercepted by can_use_tool")
else:
print("\n ⚠️ AskUserQuestion was not called (Claude may have answered directly)")
print(" This is OK — it means Claude didn't need to ask.")
return 0
finally:
remove_tmpdir(tmpdir)
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))