תיעוד 175
טיפול באישורים ובקלט משתמש
הצג למשתמשים את בקשות האישור ושאלות ההבהרה של קלוד, ואז החזר את ההחלטות שלהם ל-SDK.
בזמן עבודה על משימה, קלוד לפעמים צריך לפנות למשתמשים. ייתכן שהוא צריך הרשאה לפני מחיקת קבצים, או לשאול באיזה מסד נתונים להשתמש לפרויקט חדש. האפליקציה שלך צריכה להציג את הבקשות האלה למשתמשים כדי שקלוד יוכל להמשיך עם הקלט שלהם.
קלוד מבקש קלט משתמש בשני מצבים: כשהוא צריך הרשאה להשתמש בכלי (כמו מחיקת קבצים או הרצת פקודות), וכשיש לו שאלות הבהרה (דרך הכלי AskUserQuestion). שניהם מפעילים את ה-callback canUseTool שלך, שעוצר את הביצוע עד שאתה מחזיר תגובה. זה שונה מתורות שיחה רגילים שבהם קלוד מסיים ומחכה להודעה הבאה שלך.
לשאלות הבהרה, קלוד מייצר את השאלות ואת האפשרויות. התפקיד שלך הוא להציג אותן למשתמשים ולהחזיר את הבחירות שלהם. אי אפשר להוסיף שאלות משלך לזרימה הזו. אם אתה צריך לשאול את המשתמשים משהו בעצמך, עשה זאת בנפרד בלוגיקה של האפליקציה.
ה-callback יכול להישאר ממתין ללא הגבלת זמן. הביצוע נשאר מושהה עד שה-callback שלך חוזר. אם משתמש עלול לקחת יותר זמן להגיב ממה שהתהליך שלך יכול להישאר רץ באופן סביר, רשום hook מסוג PreToolUse שמחזיר את החלטת defer במקום להמתין ב-callback, כדי שהתהליך יוכל לצאת ולהתחדש מאוחר יותר מהסשן שנשמר.
המדריך הזה מראה איך לזהות כל סוג בקשה ולהגיב בהתאם.
#זיהוי מתי קלוד צריך קלט
העבר callback מסוג canUseTool באפשרויות השאילתה. ה-callback מופעל בכל פעם שקלוד צריך קלט משתמש, ומקבל את שם הכלי ואת הקלט כארגומנטים:
from claude_agent_sdk import ClaudeAgentOptions
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)import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";
const handleToolRequest: CanUseTool = async (toolName, input, options) => {
// options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
// Prompt the user here, then return allow or deny
return { behavior: "deny", message: "User declined" };
};
const options = { canUseTool: handleToolRequest };ה-callback מופעל בשני מקרים:
- הכלי צריך אישור: קלוד רוצה להשתמש בכלי שלא אושר אוטומטית על ידי כלל הרשאה או מצב הרשאה. בדוק את
tool_nameעבור הכלי (לדוגמה,"Bash","Write"). - קלוד שואל שאלה: קלוד קורא לכלי
AskUserQuestion. בדוק אםtool_name == "AskUserQuestion"כדי לטפל בזה אחרת. אם אתה מציין מערךtools, כלול אתAskUserQuestionכדי שזה יעבוד. למידע נוסף, ראה טיפול בשאלות הבהרה.
[!WARNING]
שלב קודם יכול לאשר או לדחות קריאה לפני ש-
canUseToolרץ. זרימת הערכת ההרשאות בודקת כללי דחייה, את מצב ההרשאה, וכללי אישור לפני ה-callback שלך.כדי להריץ את הבדיקות שלך על קריאות שכלל או מצב ההרשאה כבר אישרו, השתמש ב-hook מסוג
PreToolUse, שרץ עבור הקריאות האלה ויכול לאשר, לדחות או לשנות אותן.
אפשר גם להשתמש ב-hook מסוג PermissionRequest כדי לשלוח התראות חיצוניות (Slack, אימייל, push) כשקלוד ממתין לאישור.
#טיפול בבקשות אישור לכלי
אחרי שהעברת callback מסוג canUseTool באפשרויות השאילתה, הוא מופעל כשקלוד רוצה להשתמש בכלי ששום דבר מוקדם יותר בזרימת ההרשאות לא אישר. בחלק מההגדרות, כמו מצב dontAsk, Claude Code לא קורא לו. השלב האחרון של איך הרשאות מוערכות מפרט אותן ואומר מה קורה לקריאה במקום זאת.
ה-callback שלך מקבל שלושה ארגומנטים:
| ארגומנט | תיאור |
|---|---|
toolName | שם הכלי שקלוד רוצה להשתמש בו (לדוגמה, "Bash", "Write", "Edit") |
input | הפרמטרים שקלוד מעביר לכלי. התוכן משתנה לפי הכלי. |
options (TS) / context (Python) | הקשר נוסף שכולל suggestions אופציונלי (רשומות PermissionUpdate מוצעות כדי להימנע משאילתה חוזרת) ואות ביטול. ב-TypeScript, signal הוא AbortSignal. ב-Python, שדה האות שמור לשימוש עתידי. ראה ToolPermissionContext עבור Python. |
אובייקט input מכיל פרמטרים ייחודיים לכלי. דוגמאות נפוצות:
| כלי | שדות קלט |
|---|---|
Bash | command, description, timeout |
Write | file_path, content |
Edit | file_path, old_string, new_string |
Read | file_path, offset, limit |
ראה את חומר העזר של ה-SDK לסכמות קלט מלאות: Python | TypeScript.
אפשר להציג את המידע הזה למשתמש כדי שיוכל להחליט אם לאשר או לדחות את הפעולה, ואז להחזיר את התגובה המתאימה.
הדוגמה הבאה מבקשת מקלוד ליצור ולמחוק קובץ בדיקה. כשקלוד מנסה כל פעולה, ה-callback מדפיס את בקשת הכלי לטרמינל ומבקש אישור y/n.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from 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_tool
async 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 terminal
function 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 נחשב לדחייה. בפועל, אפשר לבנות ממשק עשיר יותר שמאפשר למשתמשים לשנות את הבקשה, לתת משוב, או להפנות את קלוד לגמרי. ראה תגובה לבקשות כלי לכל הדרכים שאפשר להגיב בהן.
#תגובה לבקשות כלי
ה-callback שלך מחזיר אחד משני סוגי תגובה:
| תגובה | Python | TypeScript |
|---|---|---|
| Allow | PermissionResultAllow(updated_input=...) | { behavior: "allow", updatedInput } |
| Deny | PermissionResultDeny(message=...) | { behavior: "deny", message } |
כשמאשרים, הכלי רץ עם הקלט שקלוד ביקש, אלא אם מחזירים קלט ששונה, updatedInput ב-TypeScript או updated_input ב-Python. לפני v2.1.207, Claude Code דחה תוצאת אישור שהשמיטה את updatedInput ודחה את קריאת הכלי עם שגיאת ולידציה.
כשדוחים, ספק הודעה שמסבירה למה. קלוד רואה את ההודעה הזו ועשוי להתאים את הגישה שלו.
מעבר לאישור או דחייה, אפשר לשנות את הקלט של הכלי או לספק הקשר שעוזר לקלוד להתאים את הגישה שלו:
- אישור: לתת לכלי לרוץ כפי שקלוד ביקש
- אישור עם שינויים: לשנות את הקלט לפני הביצוע (לדוגמה, לנקות נתיבים, להוסיף אילוצים)
- אישור וזכירה: להחזיר כלל הרשאה מוצע כדי שקריאות תואמות ידלגו על השאילה בפעם הבאה
- דחייה: לחסום את הכלי ולומר לקלוד למה
- הצעת חלופה: לחסום אבל להנחות את קלוד למה שהמשתמש רוצה במקום זאת
- הפניה מלאה: להשתמש ב-קלט בזרימה כדי לשלוח לקלוד הוראה חדשה לגמרי
העוזרים ask_user ו-askUser בקטעים הבאים מייצגים את ממשק השאילה של האפליקציה שלך.
#אישור
המשתמש מאשר את הפעולה כפי שהיא. העבר את input מה-callback שלך ללא שינוי והכלי רץ בדיוק כפי שקלוד ביקש.
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" };
};#אישור עם שינויים
המשתמש מאשר אבל רוצה לשנות את הבקשה קודם. אפשר לשנות את הקלט לפני שהכלי רץ. קלוד רואה את התוצאה אבל לא נאמר לו ששינית משהו. שימושי לניקוי פרמטרים, הוספת אילוצים, או תיחום גישה.
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 כדי שסשנים עתידיים ידלגו על השאילה עבור קריאות תואמות.
ב-TypeScript, דלג על בחירת ה-always-allow לבקשה שהאפשרויות שלה נושאות suppressAlwaysAllowRule: true. הרמז דורש Agent SDK v0.3.268 או מאוחר יותר, וה-context ב-Python לא נושא אותו.
דוגמת 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" };
};#דחייה
המשתמש לא רוצה שהפעולה הזו תקרה. חסום את הכלי וספק הודעה שמסבירה למה. קלוד רואה את ההודעה הזו ועשוי לנסות גישה אחרת.
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 };
};#הצעת חלופה
המשתמש לא רוצה את הפעולה הספציפית הזו, אבל יש לו רעיון אחר. חסום את הכלי וכלול הנחיה בהודעה. קלוד יקרא את זה ויחליט איך להמשיך לפי המשוב שלך.
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 צריך יותר כיוון במשימה עם כמה גישות תקפות, הוא קורא לכלי AskUserQuestion. זה מפעיל את ה-callback canUseTool שלך כש-toolName מוגדר ל-AskUserQuestion. הקלט מכיל את השאלות של Claude כאפשרויות בחירה, שאתה מציג למשתמש ומחזיר את הבחירות שלו.
[!TIP]
שאלות הבהרה נפוצות במיוחד במצב
plan, שבו Claude בוחן את בסיס הקוד ושואל שאלות לפני שהוא מציע תוכנית. זה הופך את מצב plan למתאים במיוחד לתהליכי עבודה אינטראקטיביים שבהם אתה רוצה ש-Claude יאסוף דרישות לפני ביצוע שינויים.
השלבים הבאים מראים איך לטפל בשאלות הבהרה:
1. העברת 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) => {
// Placeholder that approves every call. The Detect AskUserQuestion step replaces it.
return { behavior: "allow", updatedInput: input };
}
}
})) {
console.log(message);
}2. זיהוי 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);
};3. ניתוח קלט השאלה
הקלט מכיל את השאלות של 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
}
]
}ראה פורמט השאלה לתיאור מלא של השדות.
4. איסוף תשובות מהמשתמש
הצג את השאלות למשתמש ואסוף את הבחירות שלו. איך עושים את זה תלוי באפליקציה שלך: בקשת קלט בטרמינל, טופס אינטרנט, תיבת דו-שיח בנייד, וכן הלאה.
5. החזרת תשובות ל-Claude
בנה את אובייקט answers כרשומה שבה כל מפתח הוא טקסט ה-question וכל ערך הוא ה-label של האפשרות שנבחרה:
| מאובייקט השאלה | השתמש כ |
|---|---|
שדה question (לדוגמה, "How should I format the output?") | מפתח |
שדה label של האפשרות שנבחרה (לדוגמה, "Summary") | ערך |
בשאלות עם בחירה מרובה, העבר מערך של תוויות או חבר אותן עם ", ". אם אתה תומך בקלט טקסט חופשי, השתמש בטקסט המותאם אישית של המשתמש כערך.
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"
}
}
};#פורמט השאלה
הקלט מכיל את השאלות ש-Claude יצר במערך questions. לכל שאלה יש את השדות האלה:
| שדה | תיאור |
|---|---|
question | טקסט השאלה המלא להצגה |
header | תווית קצרה לשאלה (עד 12 תווים) |
options | מערך של 2-4 אפשרויות, כל אחת עם 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)
toolConfig.askUserQuestion.previewFormat מוסיף שדה preview לכל אפשרות כדי שהאפליקציה שלך תוכל להציג הדמיה חזותית לצד ה-label. בלי ההגדרה הזו, Claude לא מייצר תצוגות מקדימות והשדה חסר.
previewFormat | preview מכיל |
|---|---|
| לא מוגדר (ברירת מחדל) | השדה חסר. Claude לא מייצר תצוגות מקדימות. |
"markdown" | אמנות ASCII ובלוקי קוד מגודרים |
"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>"
}#פורמט התשובה
החזר אובייקט answers שממפה את שדה ה-question של כל שאלה ל-label של האפשרות שנבחרה:
| שדה | תיאור |
|---|---|
questions | העבר הלאה את מערך השאלות המקורי (נדרש לעיבוד הכלי) |
answers | אובייקט שבו המפתחות הם טקסט השאלה והערכים הם התוויות שנבחרו |
response | תשובה חופשית אופציונלית שהמשתמש הקליד במקום לענות על השאלות המובנות |
בשאלות עם בחירה מרובה, העבר מערך של תוויות או חבר אותן עם ", ". לטקסט חופשי לכל שאלה, כמו אפשרות "Other", שים את הטקסט של המשתמש ב-answers[question] כפי שמוצג בתמיכה בקלט טקסט חופשי. הגדר את response רק כשממשק המשתמש שלך מאפשר למשתמש לסגור את כרטיס השאלה ולהקליד תשובה כללית שאינה תשובה לשאלה מסוימת. כש-response מוגדר, Claude מקבל "The user responded: …" במקום רשימת התשובות לכל שאלה.
{
"questions": [
// ...
],
"answers": {
"How should I format the output?": "Summary",
"Which sections should I include?": ["Introduction", "Conclusion"]
}
}#תמיכה בקלט טקסט חופשי
האפשרויות המוגדרות מראש של Claude לא תמיד מכסות את מה שהמשתמשים רוצים. כדי לאפשר למשתמשים להקליד תשובה משלהם:
- הצג אפשרות "Other" נוספת אחרי האפשרויות של Claude, שמקבלת קלט טקסט
- השתמש בטקסט המותאם אישית של המשתמש כערך התשובה (לא המילה "Other")
ראה את הדוגמה המלאה למטה למימוש מלא.
#דוגמה מלאה
Claude שואל שאלות הבהרה כשהוא צריך קלט מהמשתמש כדי להמשיך. לדוגמה, כשמבקשים ממנו עזרה בהחלטה על tech stack לאפליקציית מובייל, Claude עשוי לשאול על cross-platform מול native, העדפות backend, או פלטפורמות יעד. השאלות האלה עוזרות ל-Claude לקבל החלטות שתואמות את ההעדפות של המשתמש במקום לנחש.
הדוגמה הזו מטפלת בשאלות האלה באפליקציית טרמינל. זה מה שקורה בכל שלב:
- ניתוב הבקשה: ה-callback
canUseToolבודק אם שם הכלי הוא"AskUserQuestion"ומנתב ל-handler ייעודי - הצגת שאלות: ה-handler עובר על מערך
questionsומדפיס כל שאלה עם אפשרויות ממוספרות - איסוף קלט: המשתמש יכול להזין מספר כדי לבחור אפשרות, או להקליד טקסט חופשי ישירות (לדוגמה, "jquery", "i don't know")
- מיפוי תשובות: הקוד בודק אם הקלט מספרי (משתמש ב-label של האפשרות) או טקסט חופשי (משתמש בטקסט ישירות)
- החזרה ל-Claude: התשובה כוללת גם את מערך
questionsהמקורי וגם את מיפויanswers
שמור את גרסת TypeScript בשם ask.ts והרץ אותה עם npx tsx ask.ts, או שמור את גרסת Python בשם ask.py והרץ אותה עם python ask.py.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from 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_tool
async 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 type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline/promises";
// Helper to prompt user for input in the terminal
async 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 text
function 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 answers
async function handleAskUserQuestion(input: any): Promise<PermissionResult> {
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();#מגבלות
- סוכני משנה:
AskUserQuestionאינו זמין כרגע בסוכני משנה שנוצרים דרך כלי Agent - מגבלות שאלות: כל קריאה ל-
AskUserQuestionתומכת ב-1-4 שאלות עם 2-4 אפשרויות כל אחת
#דרכים נוספות לקבל קלט מהמשתמש
ה-callback canUseTool והכלי AskUserQuestion מכסים את רוב תרחישי האישור וההבהרה, אבל ה-SDK מציע דרכים נוספות לקבל קלט ממשתמשים:
#קלט בזרימה
השתמש בקלט בזרימה כשאתה צריך:
- לקטוע את הסוכן באמצע משימה: שלח אות ביטול או שנה כיוון בזמן ש-Claude עובד
- לספק הקשר נוסף: הוסף מידע ש-Claude צריך בלי לחכות שהוא יבקש אותו
- לבנות ממשקי צ'אט: אפשר למשתמשים לשלוח הודעות המשך במהלך פעולות ארוכות
קלט בזרימה מתאים לממשקי משתמש שיחתיים שבהם משתמשים מקיימים אינטראקציה עם הסוכן לאורך כל הביצוע, לא רק בנקודות ביקורת של אישור.
#כלים מותאמים אישית
השתמש בכלים מותאמים אישית כשאתה צריך:
- לאסוף קלט מובנה: לבנות טפסים, אשפים או תהליכי עבודה מרובי שלבים שחורגים מפורמט הבחירה המרובה של
AskUserQuestion - לשלב מערכות אישור חיצוניות: להתחבר לפלטפורמות קיימות של כרטיסים, תהליכי עבודה או אישורים
- לממש אינטראקציות ייעודיות לתחום: ליצור כלים מותאמים לצרכי האפליקציה שלך, כמו ממשקי סקירת קוד או רשימות בדיקה לפריסה
כלים מותאמים אישית נותנים לך שליטה מלאה על האינטראקציה, אבל דורשים יותר עבודת מימוש מאשר שימוש ב-callback המובנה canUseTool.
#משאבים קשורים
- הגדרת הרשאות: הגדרת מצבי הרשאה וכללים
- שליטה בביצוע עם hooks: הרצת קוד מותאם אישית בנקודות מפתח במחזור החיים של הסוכן
- הפניית TypeScript SDK: תיעוד מלא של ה-API של canUseTool