مدیریت تأییدها و ورودی کاربر
هنگامِ کار روی یک وظیفه، Claude گاهی نیاز دارد با کاربر هماهنگ شود. ممکن است پیش از حذفِ فایلها به مجوز نیاز داشته باشد، یا برای یک پروژهی جدید بپرسد کدام پایگاهداده استفاده شود. برنامهات باید این درخواستها را به کاربر نشان بدهد تا Claude بتواند با ورودیِ او ادامه دهد.
Claude در دو موقعیت درخواستِ ورودیِ کاربر میکند: وقتی به مجوزِ استفاده از یک ابزار نیاز دارد (مثل حذفِ فایلها یا اجرای دستورها)، و وقتی پرسشهای روشنگرانه دارد (از طریقِ ابزارِ AskUserQuestion). هر دو، callbackِ canUseTool تو را فعال میکنند که اجرا را تا برگرداندنِ پاسخت متوقف نگه میدارد. این با نوبتهای عادیِ گفتگو فرق دارد، جایی که Claude کار را تمام میکند و منتظرِ پیامِ بعدیِ تو میماند.
برای پرسشهای روشنگرانه، Claude خودِ پرسشها و گزینهها را تولید میکند. نقشِ تو این است که آنها را به کاربر ارائه بدهی و انتخابهایش را برگردانی. تو نمیتوانی پرسشهای خودت را به این جریان اضافه کنی؛ اگر میخواهی خودت چیزی از کاربر بپرسی، این کار را جداگانه در منطقِ برنامهات انجام بده.
callback میتواند بهصورتِ نامحدود معلق بماند. اجرا تا برگشتِ callbackت متوقف میماند، و SDK فقط وقتی انتظار را لغو میکند که خودِ پرسوجو لغو شود. اگر ممکن است کاربر دیرتر از آنچه فرآیندت میتواند بهطور معقول روشن بماند پاسخ بدهد، تصمیمِ hook بهنامِ defer را برگردان، که به فرآیند اجازه میدهد خارج شود و بعداً از نشستِ پایدارشده ادامه بدهد.
این راهنما به تو نشان میدهد چطور هر نوع درخواست را تشخیص بدهی و مناسب پاسخ بدهی.
تشخیصِ اینکه Claude کِی به ورودی نیاز دارد
Section titled “تشخیصِ اینکه Claude کِی به ورودی نیاز دارد”یک callbackِ canUseTool در گزینههای پرسوجوی خود پاس بده. این callback هر وقت Claude به ورودیِ کاربر نیاز داشته باشد فعال میشود و نامِ ابزار و ورودی را بهعنوانِ آرگومان دریافت میکند:
async def handle_tool_request(tool_name, input_data, context): # Prompt user and return allow or deny ...
options = ClaudeAgentOptions(can_use_tool=handle_tool_request)async function handleToolRequest(toolName, input, options) { // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] } // Prompt user and return allow or deny}
const options = { canUseTool: handleToolRequest };این callback در دو حالت فعال میشود:
- ابزار به تأیید نیاز دارد: Claude میخواهد از ابزاری استفاده کند که با قواعدِ دسترسی یا حالتها بهصورت خودکار تأیید نشده است.
tool_nameرا برای ابزار بررسی کن (مثلاً"Bash","Write"). - Claude پرسش میکند: Claude ابزارِ
AskUserQuestionرا صدا میزند. بررسی کن آیاtool_name == "AskUserQuestion"است تا متفاوت با آن برخورد کنی. اگر یک آرایهیtoolsمشخص میکنی،AskUserQuestionرا در آن بگنجان تا این کار کند. برای جزئیات مدیریتِ پرسشهای روشنگرانه را ببین.
مدیریتِ درخواستهای تأییدِ ابزار
Section titled “مدیریتِ درخواستهای تأییدِ ابزار”وقتی یک callbackِ canUseTool در گزینههای پرسوجویت پاس دادی، هر بار که Claude بخواهد از ابزاری استفاده کند که بهصورت خودکار تأیید نشده، فعال میشود. callbackت سه آرگومان دریافت میکند:
| آرگومان | توضیح |
|---|---|
toolName | نامِ ابزاری که Claude میخواهد استفاده کند (مثلاً "Bash", "Write", "Edit") |
input | پارامترهایی که Claude به ابزار پاس میدهد. محتوا بسته به ابزار فرق میکند. |
options (TS) / context (Python) | کانتکستِ اضافی شاملِ suggestionsِ اختیاری (ورودیهای پیشنهادیِ PermissionUpdate برای جلوگیری از پرسشِ مجدد) و یک سیگنالِ لغو. در TypeScript، signal یک AbortSignal است؛ در Python، فیلدِ signal برای استفادهی آینده رزرو شده است. برای Python ToolPermissionContext را ببین. |
شیءِ input شاملِ پارامترهای مختصِ ابزار است. نمونههای رایج:
| ابزار | فیلدهای ورودی |
|---|---|
Bash | command, description, timeout |
Write | file_path, content |
Edit | file_path, old_string, new_string |
Read | file_path, offset, limit |
برای اسکیماهای کاملِ ورودی، مرجعِ SDK را ببین: Python | TypeScript.
میتوانی این اطلاعات را به کاربر نشان بدهی تا تصمیم بگیرد عمل را اجازه بدهد یا رد کند، سپس پاسخِ مناسب را برگردانی.
مثالِ زیر از Claude میخواهد یک فایلِ آزمایشی بسازد و حذف کند. وقتی Claude هر عملیات را تلاش میکند، callback درخواستِ ابزار را در ترمینال چاپ میکند و برای تأییدِ y/n پرسش میکند.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, queryfrom claude_agent_sdk.types import ( HookMatcher, PermissionResultAllow, PermissionResultDeny, ToolPermissionContext,)
async def can_use_tool( tool_name: str, input_data: dict, context: ToolPermissionContext) -> PermissionResultAllow | PermissionResultDeny: # Display the tool request print(f"\nTool: {tool_name}") if tool_name == "Bash": print(f"Command: {input_data.get('command')}") if input_data.get("description"): print(f"Description: {input_data.get('description')}") else: print(f"Input: {input_data}")
# Get user approval response = input("Allow this action? (y/n): ")
# Return allow or deny based on user's response if response.lower() == "y": # Allow: tool executes with the original (or modified) input return PermissionResultAllow(updated_input=input_data) else: # Deny: tool doesn't execute, Claude sees the message return PermissionResultDeny(message="User denied this action")
# Required workaround: dummy hook keeps the stream open for can_use_toolasync def dummy_hook(input_data, tool_use_id, context): return {"continue_": True}
async def prompt_stream(): yield { "type": "user", "message": { "role": "user", "content": "Create a test file in /tmp and then delete it", }, }
async def main(): async for message in query( prompt=prompt_stream(), options=ClaudeAgentOptions( can_use_tool=can_use_tool, hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]}, ), ): if isinstance(message, ResultMessage) and message.subtype == "success": print(message.result)
asyncio.run(main())import { query } from "@anthropic-ai/claude-agent-sdk";import * as readline from "readline";
// Helper to prompt user for input in the terminalfunction prompt(question: string): Promise<string> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }) );}
for await (const message of query({ prompt: "Create a test file in /tmp and then delete it", options: { canUseTool: async (toolName, input) => { // Display the tool request console.log(`\nTool: ${toolName}`); if (toolName === "Bash") { console.log(`Command: ${input.command}`); if (input.description) console.log(`Description: ${input.description}`); } else { console.log(`Input: ${JSON.stringify(input, null, 2)}`); }
// Get user approval const response = await prompt("Allow this action? (y/n): ");
// Return allow or deny based on user's response if (response.toLowerCase() === "y") { // Allow: tool executes with the original (or modified) input return { behavior: "allow", updatedInput: input }; } else { // Deny: tool doesn't execute, Claude sees the message return { behavior: "deny", message: "User denied this action" }; } } }})) { if ("result" in message) console.log(message.result);}این مثال از یک جریانِ y/n استفاده میکند که هر ورودیِ غیر از y بهعنوانِ رد تلقی میشود. در عمل، ممکن است یک UI غنیتر بسازی که به کاربران اجازه دهد درخواست را تغییر دهند، بازخورد بدهند، یا Claude را بهکلی به مسیرِ دیگری هدایت کنند. برای همهی راههایی که میتوانی پاسخ بدهی پاسخ به درخواستهای ابزار را ببین.
پاسخ به درخواستهای ابزار
Section titled “پاسخ به درخواستهای ابزار”callbackت یکی از دو نوعِ پاسخ را برمیگرداند:
| پاسخ | Python | TypeScript |
|---|---|---|
| Allow | PermissionResultAllow(updated_input=...) | { behavior: "allow", updatedInput } |
| Deny | PermissionResultDeny(message=...) | { behavior: "deny", message } |
هنگامِ اجازهدادن، ورودیِ ابزار (اصلی یا تغییریافته) را پاس بده. هنگامِ رد، پیامی بده که دلیل را توضیح میدهد. Claude این پیام را میبیند و ممکن است رویکردش را تنظیم کند.
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
# Allow the tool to executereturn PermissionResultAllow(updated_input=input_data)
# Block the toolreturn PermissionResultDeny(message="User rejected this action")// Allow the tool to executereturn { behavior: "allow", updatedInput: input };
// Block the toolreturn { behavior: "deny", message: "User rejected this action" };فراتر از اجازهدادن یا رد، میتوانی ورودیِ ابزار را تغییر دهی یا کانتکستی بدهی که به Claude کمک کند رویکردش را تنظیم کند:
- تأیید: بگذار ابزار همانطور که Claude درخواست کرد اجرا شود
- تأیید با تغییرات: ورودی را پیش از اجرا تغییر بده (مثلاً پاکسازیِ مسیرها، افزودنِ محدودیت)
- تأیید و بهخاطر سپردن: یک قاعدهی دسترسیِ پیشنهادی را بازتاب بده تا فراخوانیهای منطبق دفعهی بعد پرسش را رد کنند
- رد: ابزار را مسدود کن و به Claude بگو چرا
- پیشنهادِ جایگزین: مسدود کن اما Claude را بهسمتِ چیزی که کاربر میخواهد هدایت کن
- هدایتِ کامل: از ورودیِ استریمینگ استفاده کن تا یک دستورالعملِ کاملاً جدید به Claude بفرستی
کاربر عمل را همانطور که هست تأیید میکند. input را از callbackت بدونِ تغییر پاس بده و ابزار دقیقاً همانطور که Claude درخواست کرد اجرا میشود.
async def can_use_tool(tool_name, input_data, context): print(f"Claude wants to use {tool_name}") approved = await ask_user("Allow this action?")
if approved: return PermissionResultAllow(updated_input=input_data) return PermissionResultDeny(message="User declined")canUseTool: async (toolName, input) => { console.log(`Claude wants to use ${toolName}`); const approved = await askUser("Allow this action?");
if (approved) { return { behavior: "allow", updatedInput: input }; } return { behavior: "deny", message: "User declined" };};کاربر تأیید میکند اما میخواهد ابتدا درخواست را تغییر دهد. میتوانی ورودی را پیش از اجرای ابزار عوض کنی. Claude نتیجه را میبیند اما به او گفته نمیشود که چیزی را تغییر دادهای. برای پاکسازیِ پارامترها، افزودنِ محدودیتها، یا محدودکردنِ دسترسی مفید است.
async def can_use_tool(tool_name, input_data, context): if tool_name == "Bash": # User approved, but scope all commands to sandbox sandboxed_input = {**input_data} sandboxed_input["command"] = input_data["command"].replace( "/tmp", "/tmp/sandbox" ) return PermissionResultAllow(updated_input=sandboxed_input) return PermissionResultAllow(updated_input=input_data)canUseTool: async (toolName, input) => { if (toolName === "Bash") { // User approved, but scope all commands to sandbox const sandboxedInput = { ...input, command: input.command.replace("/tmp", "/tmp/sandbox") }; return { behavior: "allow", updatedInput: sandboxedInput }; } return { behavior: "allow", updatedInput: input };};کاربر تأیید میکند و نمیخواهد دوباره برای این نوع فراخوانی از او پرسیده شود. آرگومانِ سومِ callback شاملِ suggestions است، آرایهای از ورودیهای آمادهی PermissionUpdate. یکی را در updatedPermissions بازتاب بده تا اعمالش کنی. پیشنهادی با مقصدِ localSettings قاعده را در .claude/settings.local.json مینویسد تا نشستهای آینده برای فراخوانیهای منطبق پرسش را رد کنند.
مثالِ Python نیازمندِ claude-agent-sdk نسخهی 0.1.80 یا بالاتر است.
async def can_use_tool(tool_name, input_data, context): choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
if choice == "always": persist = [ s for s in context.suggestions if s.destination == "localSettings" ] return PermissionResultAllow( updated_input=input_data, updated_permissions=persist ) if choice == "once": return PermissionResultAllow(updated_input=input_data) return PermissionResultDeny(message="User declined")canUseTool: async (toolName, input, { suggestions = [] }) => { const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
if (choice === "always") { const persist = suggestions.filter( (s) => s.destination === "localSettings" ); return { behavior: "allow", updatedInput: input, updatedPermissions: persist }; } if (choice === "once") { return { behavior: "allow", updatedInput: input }; } return { behavior: "deny", message: "User declined" };};کاربر نمیخواهد این عمل اتفاق بیفتد. ابزار را مسدود کن و پیامی بده که دلیل را توضیح میدهد. Claude این پیام را میبیند و ممکن است رویکردِ دیگری امتحان کند.
async def can_use_tool(tool_name, input_data, context): approved = await ask_user(f"Allow {tool_name}?")
if not approved: return PermissionResultDeny(message="User rejected this action") return PermissionResultAllow(updated_input=input_data)canUseTool: async (toolName, input) => { const approved = await askUser(`Allow ${toolName}?`);
if (!approved) { return { behavior: "deny", message: "User rejected this action" }; } return { behavior: "allow", updatedInput: input };};کاربر این عملِ خاص را نمیخواهد، اما ایدهی دیگری دارد. ابزار را مسدود کن و راهنمایی را در پیامت بگنجان. Claude این را میخواند و بر اساسِ بازخوردِ تو تصمیم میگیرد چطور پیش برود.
async def can_use_tool(tool_name, input_data, context): if tool_name == "Bash" and "rm" in input_data.get("command", ""): # User doesn't want to delete, suggest archiving instead return PermissionResultDeny( message="User doesn't want to delete files. They asked if you could compress them into an archive instead." ) return PermissionResultAllow(updated_input=input_data)canUseTool: async (toolName, input) => { if (toolName === "Bash" && input.command.includes("rm")) { // User doesn't want to delete, suggest archiving instead return { behavior: "deny", message: "User doesn't want to delete files. They asked if you could compress them into an archive instead." }; } return { behavior: "allow", updatedInput: input };};برای یک تغییرِ کاملِ مسیر (نه فقط یک تلنگر)، از ورودیِ استریمینگ استفاده کن تا مستقیماً یک دستورالعملِ جدید به Claude بفرستی. این درخواستِ ابزارِ فعلی را دور میزند و دستورالعملهای کاملاً جدیدی به Claude میدهد تا دنبال کند.
مدیریتِ پرسشهای روشنگرانه
Section titled “مدیریتِ پرسشهای روشنگرانه”وقتی Claude برای وظیفهای با چند رویکردِ معتبر به جهتگیریِ بیشتری نیاز دارد، ابزارِ AskUserQuestion را صدا میزند. این callbackِ canUseTool تو را با toolName برابرِ AskUserQuestion فعال میکند. ورودی شاملِ پرسشهای Claude بهصورتِ گزینههای چندگزینهای است که آنها را به کاربر نشان میدهی و انتخابهایش را برمیگردانی.
گامهای زیر نشان میدهند چطور پرسشهای روشنگرانه را مدیریت کنی:
یک callbackِ canUseTool پاس بده
یک callbackِ canUseTool در گزینههای پرسوجویت پاس بده. بهصورتِ پیشفرض، AskUserQuestion در دسترس است. اگر یک آرایهی tools مشخص میکنی تا تواناییهای Claude را محدود کنی (مثلاً یک ایجنتِ فقطخواندنی با فقط Read، Glob و Grep)، AskUserQuestion را در آن آرایه بگنجان. وگرنه Claude نمیتواند پرسشهای روشنگرانه بپرسد:
async for message in query( prompt="Analyze this codebase", options=ClaudeAgentOptions( # Include AskUserQuestion in your tools list tools=["Read", "Glob", "Grep", "AskUserQuestion"], can_use_tool=can_use_tool, ),): print(message)for await (const message of query({ prompt: "Analyze this codebase", options: { // Include AskUserQuestion in your tools list tools: ["Read", "Glob", "Grep", "AskUserQuestion"], canUseTool: async (toolName, input) => { // Handle clarifying questions here } }})) { console.log(message);}AskUserQuestion را تشخیص بده
در callbackت، بررسی کن آیا toolName برابرِ AskUserQuestion است تا متفاوت با سایرِ ابزارها با آن برخورد کنی:
async def can_use_tool(tool_name: str, input_data: dict, context): if tool_name == "AskUserQuestion": # Your implementation to collect answers from the user return await handle_clarifying_questions(input_data) # Handle other tools normally return await prompt_for_approval(tool_name, input_data)canUseTool: async (toolName, input) => { if (toolName === "AskUserQuestion") { // Your implementation to collect answers from the user return handleClarifyingQuestions(input); } // Handle other tools normally return promptForApproval(toolName, input);};ورودیِ پرسش را تجزیه کن
ورودی شاملِ پرسشهای Claude در یک آرایهی questions است. هر پرسش یک question (متنی که نمایش داده میشود)، options (گزینهها)، و multiSelect (اینکه آیا چند انتخاب مجاز است) دارد:
{ "questions": [ { "question": "How should I format the output?", "header": "Format", "options": [ { "label": "Summary", "description": "Brief overview" }, { "label": "Detailed", "description": "Full explanation" } ], "multiSelect": false }, { "question": "Which sections should I include?", "header": "Sections", "options": [ { "label": "Introduction", "description": "Opening context" }, { "label": "Conclusion", "description": "Final summary" } ], "multiSelect": true } ]}برای توضیحِ کاملِ فیلدها قالبِ پرسش را ببین.
پاسخها را از کاربر جمعآوری کن
پرسشها را به کاربر ارائه بده و انتخابهایش را جمعآوری کن. نحوهی این کار به برنامهات بستگی دارد: یک پرامپتِ ترمینال، یک فرمِ وب، یک دیالوگِ موبایل، و جز اینها.
پاسخها را به Claude برگردان
شیءِ answers را بهصورتِ یک رکورد بساز که هر کلید، متنِ question و هر مقدار، labelِ گزینهی انتخابشده است:
| از شیءِ پرسش | بهعنوانِ |
|---|---|
فیلدِ question (مثلاً "How should I format the output?") | کلید |
فیلدِ labelِ گزینهی انتخابشده (مثلاً "Summary") | مقدار |
برای پرسشهای چندانتخابی، یک آرایه از labelها پاس بده یا آنها را با ", " به هم بچسبان. اگر از ورودیِ متنِ آزاد پشتیبانی میکنی، از متنِ سفارشیِ کاربر بهعنوانِ مقدار استفاده کن.
return PermissionResultAllow( updated_input={ "questions": input_data.get("questions", []), "answers": { "How should I format the output?": "Summary", "Which sections should I include?": ["Introduction", "Conclusion"], }, })return { behavior: "allow", updatedInput: { questions: input.questions, answers: { "How should I format the output?": "Summary", "Which sections should I include?": "Introduction, Conclusion" } }};قالبِ پرسش
Section titled “قالبِ پرسش”ورودی شاملِ پرسشهای تولیدشدهی Claude در یک آرایهی questions است. هر پرسش این فیلدها را دارد:
| فیلد | توضیح |
|---|---|
question | متنِ کاملِ پرسش برای نمایش |
header | برچسبِ کوتاه برای پرسش (حداکثر ۱۲ کاراکتر) |
options | آرایهای از ۲ تا ۴ گزینه، هرکدام با label و description. در TypeScript: بهصورتِ اختیاری preview (به پایین نگاه کن) |
multiSelect | اگر true باشد، کاربران میتوانند چند گزینه انتخاب کنند |
ساختاری که callbackت دریافت میکند:
{ "questions": [ { "question": "How should I format the output?", "header": "Format", "options": [ { "label": "Summary", "description": "Brief overview of key points" }, { "label": "Detailed", "description": "Full explanation with examples" } ], "multiSelect": false } ]}پیشنمایشِ گزینهها (TypeScript)
Section titled “پیشنمایشِ گزینهها (TypeScript)”toolConfig.askUserQuestion.previewFormat یک فیلدِ preview به هر گزینه اضافه میکند تا برنامهات بتواند کنارِ label یک ماکآپِ بصری نشان دهد. بدونِ این تنظیم، Claude پیشنمایش تولید نمیکند و این فیلد غایب است.
previewFormat | preview شاملِ |
|---|---|
| تنظیمنشده (پیشفرض) | فیلد غایب است. Claude پیشنمایش تولید نمیکند. |
"markdown" | ASCII art و بلاکهای کدِ محصورشده |
"html" | یک قطعهی <div>ِ استایلخورده (SDK تگهای <script>, <style> و <!DOCTYPE> را پیش از اجرای callbackت رد میکند) |
این قالب برای همهی پرسشهای نشست اعمال میشود. Claude preview را روی گزینههایی میگنجاند که مقایسهی بصری کمککننده است (انتخابهای چیدمان، طرحِ رنگها) و جایی که کمکی نمیکند حذفش میکند (تأییدهای بله/خیر، انتخابهای فقطمتنی). پیش از رندر، undefined بودن را بررسی کن.
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({ prompt: "Help me choose a card layout", options: { toolConfig: { askUserQuestion: { previewFormat: "html" } }, canUseTool: async (toolName, input) => { // input.questions[].options[].preview is an HTML string or undefined return { behavior: "allow", updatedInput: input }; } }})) { // ...}یک گزینه با پیشنمایشِ HTML:
{ "label": "Compact", "description": "Title and metric value only", "preview": "<div style=\"padding:12px;border:1px solid #ddd;border-radius:8px\"><div style=\"font-size:12px;color:#666\">Active users</div><div style=\"font-size:28px;font-weight:600\">1,284</div></div>"}قالبِ پاسخ
Section titled “قالبِ پاسخ”یک شیءِ answers برگردان که فیلدِ questionِ هر پرسش را به labelِ گزینهی انتخابشده نگاشت میکند:
| فیلد | توضیح |
|---|---|
questions | آرایهی پرسشهای اصلی را عیناً پاس بده (برای پردازشِ ابزار لازم است) |
answers | شیئی که کلیدهایش متنِ پرسش و مقادیرش labelهای انتخابشدهاند |
response | پاسخِ آزادِ اختیاری که کاربر بهجای پاسخ به پرسشهای ساختاریافته تایپ کرده است |
برای پرسشهای چندانتخابی، یک آرایه از labelها پاس بده یا آنها را با ", " به هم بچسبان. برای متنِ آزادِ هر-پرسش مثل گزینهی «Other»، متنِ کاربر را در answers[question] بگذار، همانطور که در پشتیبانی از ورودیِ متنِ آزاد نشان داده شده. response را فقط وقتی تنظیم کن که UI تو به کاربر اجازه میدهد کارتِ پرسش را کنار بزند و یک پاسخِ کلی تایپ کند که جوابِ هیچ پرسشِ خاصی نیست. وقتی response تنظیم میشود، Claude بهجای فهرستِ پاسخِ هر-پرسش، «The user responded: …» را دریافت میکند.
{ "questions": [ // ... ], "answers": { "How should I format the output?": "Summary", "Which sections should I include?": ["Introduction", "Conclusion"] }}پشتیبانی از ورودیِ متنِ آزاد
Section titled “پشتیبانی از ورودیِ متنِ آزاد”گزینههای از پیشتعریفشدهی Claude همیشه چیزی را که کاربران میخواهند پوشش نمیدهند. برای اینکه به کاربران اجازه دهی پاسخِ خودشان را تایپ کنند:
- یک گزینهی اضافیِ «Other» بعد از گزینههای Claude نمایش بده که ورودیِ متنی میپذیرد
- از متنِ سفارشیِ کاربر بهعنوانِ مقدارِ پاسخ استفاده کن (نه واژهی «Other»)
برای پیادهسازیِ کامل مثالِ کاملِ پایین را ببین.
مثالِ کامل
Section titled “مثالِ کامل”Claude وقتی برای پیشرفتن به ورودیِ کاربر نیاز دارد پرسشهای روشنگرانه میپرسد. مثلاً، وقتی از او خواسته میشود برای انتخابِ یک tech stack برای یک اپِ موبایل کمک کند، Claude ممکن است دربارهی چندسکویی در برابرِ بومی، ترجیحاتِ backend، یا پلتفرمهای هدف بپرسد. این پرسشها به Claude کمک میکنند تصمیماتی بگیرد که با ترجیحاتِ کاربر همخوان باشد بهجای حدسزدن.
این مثال آن پرسشها را در یک برنامهی ترمینالی مدیریت میکند. این چیزی است که در هر گام اتفاق میافتد:
- مسیریابیِ درخواست: callbackِ
canUseToolبررسی میکند آیا نامِ ابزار"AskUserQuestion"است و به یک هندلرِ اختصاصی مسیر میدهد - نمایشِ پرسشها: هندلر روی آرایهی
questionsحلقه میزند و هر پرسش را با گزینههای شمارهگذاریشده چاپ میکند - جمعآوریِ ورودی: کاربر میتواند یک شماره برای انتخابِ یک گزینه وارد کند، یا متنِ آزاد را مستقیماً تایپ کند (مثلاً «jquery»، «i don’t know»)
- نگاشتِ پاسخها: کد بررسی میکند آیا ورودی عددی است (از labelِ گزینه استفاده میکند) یا متنِ آزاد (مستقیماً از متن استفاده میکند)
- برگشت به Claude: پاسخ هم آرایهی اصلیِ
questionsو هم نگاشتِanswersرا در بر دارد
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, queryfrom claude_agent_sdk.types import HookMatcher, PermissionResultAllow
def parse_response(response: str, options: list) -> str: """Parse user input as option number(s) or free text.""" try: indices = [int(s.strip()) - 1 for s in response.split(",")] labels = [options[i]["label"] for i in indices if 0 <= i < len(options)] return ", ".join(labels) if labels else response except ValueError: return response
async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow: """Display Claude's questions and collect user answers.""" answers = {}
for q in input_data.get("questions", []): print(f"\n{q['header']}: {q['question']}")
options = q["options"] for i, opt in enumerate(options): print(f" {i + 1}. {opt['label']} - {opt['description']}") if q.get("multiSelect"): print(" (Enter numbers separated by commas, or type your own answer)") else: print(" (Enter a number, or type your own answer)")
response = input("Your choice: ").strip() answers[q["question"]] = parse_response(response, options)
return PermissionResultAllow( updated_input={ "questions": input_data.get("questions", []), "answers": answers, } )
async def can_use_tool( tool_name: str, input_data: dict, context) -> PermissionResultAllow: # Route AskUserQuestion to our question handler if tool_name == "AskUserQuestion": return await handle_ask_user_question(input_data) # Auto-approve other tools for this example return PermissionResultAllow(updated_input=input_data)
async def prompt_stream(): yield { "type": "user", "message": { "role": "user", "content": "Help me decide on the tech stack for a new mobile app", }, }
# Required workaround: dummy hook keeps the stream open for can_use_toolasync def dummy_hook(input_data, tool_use_id, context): return {"continue_": True}
async def main(): async for message in query( prompt=prompt_stream(), options=ClaudeAgentOptions( can_use_tool=can_use_tool, hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]}, ), ): if isinstance(message, ResultMessage) and message.subtype == "success": print(message.result)
asyncio.run(main())import { query } from "@anthropic-ai/claude-agent-sdk";import * as readline from "readline/promises";
// Helper to prompt user for input in the terminalasync function prompt(question: string): Promise<string> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await rl.question(question); rl.close(); return answer;}
// Parse user input as option number(s) or free textfunction parseResponse(response: string, options: any[]): string { const indices = response.split(",").map((s) => parseInt(s.trim()) - 1); const labels = indices .filter((i) => !isNaN(i) && i >= 0 && i < options.length) .map((i) => options[i].label); return labels.length > 0 ? labels.join(", ") : response;}
// Display Claude's questions and collect user answersasync function handleAskUserQuestion(input: any) { const answers: Record<string, string> = {};
for (const q of input.questions) { console.log(`\n${q.header}: ${q.question}`);
const options = q.options; options.forEach((opt: any, i: number) => { console.log(` ${i + 1}. ${opt.label} - ${opt.description}`); }); if (q.multiSelect) { console.log(" (Enter numbers separated by commas, or type your own answer)"); } else { console.log(" (Enter a number, or type your own answer)"); }
const response = (await prompt("Your choice: ")).trim(); answers[q.question] = parseResponse(response, options); }
// Return the answers to Claude (must include original questions) return { behavior: "allow", updatedInput: { questions: input.questions, answers } };}
async function main() { for await (const message of query({ prompt: "Help me decide on the tech stack for a new mobile app", options: { canUseTool: async (toolName, input) => { // Route AskUserQuestion to our question handler if (toolName === "AskUserQuestion") { return handleAskUserQuestion(input); } // Auto-approve other tools for this example return { behavior: "allow", updatedInput: input }; } } })) { if ("result" in message) console.log(message.result); }}
main();محدودیتها
Section titled “محدودیتها”- سابایجنتها:
AskUserQuestionدر حالِ حاضر در سابایجنتهایی که از طریقِ ابزارِ Agent ساخته میشوند در دسترس نیست - محدودیتِ پرسش: هر فراخوانیِ
AskUserQuestionاز ۱ تا ۴ پرسش با ۲ تا ۴ گزینه برای هرکدام پشتیبانی میکند
راههای دیگرِ گرفتنِ ورودی از کاربر
Section titled “راههای دیگرِ گرفتنِ ورودی از کاربر”callbackِ canUseTool و ابزارِ AskUserQuestion بیشترِ سناریوهای تأیید و روشنسازی را پوشش میدهند، اما SDK راههای دیگری هم برای گرفتنِ ورودی از کاربر ارائه میدهد:
ورودیِ استریمینگ
Section titled “ورودیِ استریمینگ”از ورودیِ استریمینگ وقتی استفاده کن که نیاز داری:
- ایجنت را وسطِ وظیفه قطع کنی: یک سیگنالِ لغو بفرستی یا حین کارِ Claude مسیر را عوض کنی
- کانتکستِ اضافی بدهی: اطلاعاتی را که Claude نیاز دارد اضافه کنی بدونِ اینکه منتظرِ پرسیدنش بمانی
- رابطهای چت بسازی: به کاربران اجازه بدهی حینِ عملیاتِ طولانی پیامهای پیگیری بفرستند
ورودیِ استریمینگ برای UIهای گفتگومحور ایدهآل است، جایی که کاربران در طولِ اجرا با ایجنت تعامل میکنند، نه فقط در نقاطِ بازرسیِ تأیید.
ابزارهای سفارشی
Section titled “ابزارهای سفارشی”از ابزارهای سفارشی وقتی استفاده کن که نیاز داری:
- ورودیِ ساختاریافته جمعآوری کنی: فرمها، ویزاردها، یا ورکفلوهای چندگامی بسازی که فراتر از قالبِ چندگزینهایِ
AskUserQuestionبروند - سیستمهای تأییدِ بیرونی را یکپارچه کنی: به پلتفرمهای موجودِ تیکتینگ، ورکفلو، یا تأیید وصل شوی
- تعاملهای مختصِ دامنه پیادهسازی کنی: ابزارهایی متناسب با نیازهای برنامهات بسازی، مثل رابطهای بازبینیِ کد یا چکلیستهای استقرار
ابزارهای سفارشی کنترلِ کاملی بر تعامل به تو میدهند، اما نسبت به استفاده از callbackِ توکارِ canUseTool کارِ پیادهسازیِ بیشتری میطلبند.
منابعِ مرتبط
Section titled “منابعِ مرتبط”- پیکربندیِ دسترسیها: حالتها و قواعدِ دسترسی را تنظیم کن
- کنترلِ اجرا با hookها: کدِ سفارشی را در نقاطِ کلیدیِ چرخهی حیاتِ ایجنت اجرا کن
- مرجعِ TypeScript SDK: مستنداتِ کاملِ API بهنامِ canUseTool