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
This commit is contained in:
@@ -162,6 +162,12 @@ class SessionManager:
|
||||
if session and session.sdk_session:
|
||||
await session.sdk_session.approve(approved)
|
||||
|
||||
async def answer_question(self, conv_id: str, answers: dict[str, str]) -> None:
|
||||
"""Resolve a pending AskUserQuestion with user's answers."""
|
||||
session = self._sessions.get(conv_id)
|
||||
if session and session.sdk_session:
|
||||
await session.sdk_session.answer_question(answers)
|
||||
|
||||
# --- Close, list, permission ---
|
||||
|
||||
async def close(self, conv_id: str, user_id: Optional[str] = None) -> bool:
|
||||
|
||||
+68
-9
@@ -48,6 +48,7 @@ class SessionProgress:
|
||||
last_result: str = ""
|
||||
error: str = ""
|
||||
pending_approval: str = "" # non-empty → waiting for approval, value is tool description
|
||||
pending_question: dict | None = None # non-None → waiting for user answer to AskUserQuestion
|
||||
|
||||
|
||||
class SDKSession:
|
||||
@@ -97,6 +98,10 @@ class SDKSession:
|
||||
self._pending_approval: asyncio.Future | None = None
|
||||
self._pending_approval_desc: str = ""
|
||||
|
||||
# AskUserQuestion mechanism
|
||||
self._pending_question: asyncio.Future | None = None
|
||||
self._pending_question_data: dict | None = None # {questions: [...], conv_id: ...}
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Create and connect the ClaudeSDKClient, start the message loop."""
|
||||
from agent.sdk_hooks import build_hooks
|
||||
@@ -169,6 +174,7 @@ class SDKSession:
|
||||
last_result=self._last_result[:1000],
|
||||
error=self._error,
|
||||
pending_approval=self._pending_approval_desc,
|
||||
pending_question=self._pending_question_data,
|
||||
)
|
||||
|
||||
async def interrupt(self) -> None:
|
||||
@@ -189,6 +195,15 @@ class SDKSession:
|
||||
if self._pending_approval and not self._pending_approval.done():
|
||||
self._pending_approval.set_result(approved)
|
||||
|
||||
async def answer_question(self, answers: dict[str, str]) -> None:
|
||||
"""Resolve a pending AskUserQuestion with user's selected answers.
|
||||
|
||||
Args:
|
||||
answers: maps question text → selected option label.
|
||||
"""
|
||||
if self._pending_question and not self._pending_question.done():
|
||||
self._pending_question.set_result(answers)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Disconnect and clean up."""
|
||||
if self._message_loop_task and not self._message_loop_task.done():
|
||||
@@ -280,15 +295,64 @@ class SDKSession:
|
||||
async def _permission_callback(
|
||||
self, tool_name: str, input_data: dict, context: ToolPermissionContext
|
||||
) -> PermissionResult:
|
||||
"""can_use_tool — send approval card to Feishu, wait for card callback."""
|
||||
"""can_use_tool — route to question card or approval card based on tool type."""
|
||||
# Auto-allow read-only tools
|
||||
if tool_name in ("Read", "Glob", "Grep", "WebSearch", "WebFetch"):
|
||||
return PermissionResultAllow()
|
||||
|
||||
# AskUserQuestion: show options to user, collect answer, return via updated_input
|
||||
if tool_name == "AskUserQuestion":
|
||||
return await self._handle_ask_user_question(input_data)
|
||||
|
||||
if not self.chat_id:
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Send approval card
|
||||
# Regular tools: approval flow
|
||||
return await self._handle_tool_approval(tool_name, input_data)
|
||||
|
||||
async def _handle_ask_user_question(self, input_data: dict) -> PermissionResult:
|
||||
"""Handle AskUserQuestion: send question card, wait for answer, return updated_input."""
|
||||
if not self.chat_id:
|
||||
return PermissionResultAllow()
|
||||
|
||||
questions = input_data.get("questions", [])
|
||||
if not questions:
|
||||
return PermissionResultAllow()
|
||||
|
||||
from bot.feishu import send_card, build_question_card
|
||||
|
||||
# Build and send question card
|
||||
self._pending_question_data = {"questions": questions, "conv_id": self.conv_id}
|
||||
card = build_question_card(
|
||||
conv_id=self.conv_id,
|
||||
questions=questions,
|
||||
)
|
||||
await send_card(self.chat_id, "chat_id", card)
|
||||
|
||||
# Wait for user's answer (via card callback or text reply)
|
||||
loop = asyncio.get_running_loop()
|
||||
self._pending_question = loop.create_future()
|
||||
try:
|
||||
answers = await asyncio.wait_for(
|
||||
self._pending_question, timeout=APPROVAL_TIMEOUT
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
answers = {}
|
||||
from bot.feishu import send_markdown
|
||||
await send_markdown(self.chat_id, "chat_id", "⏰ 问题超时,已跳过。")
|
||||
finally:
|
||||
self._pending_question_data = None
|
||||
|
||||
# Pre-fill answers in the tool input
|
||||
modified_input = dict(input_data)
|
||||
if "answers" not in modified_input or not isinstance(modified_input.get("answers"), dict):
|
||||
modified_input["answers"] = {}
|
||||
modified_input["answers"].update(answers)
|
||||
|
||||
return PermissionResultAllow(updated_input=modified_input)
|
||||
|
||||
async def _handle_tool_approval(self, tool_name: str, input_data: dict) -> PermissionResult:
|
||||
"""Handle regular tool approval: send approval card, wait for approve/deny."""
|
||||
from bot.feishu import send_card, build_approval_card
|
||||
|
||||
summary = self._format_tool_summary(tool_name, input_data)
|
||||
@@ -302,7 +366,6 @@ class SDKSession:
|
||||
)
|
||||
await send_card(self.chat_id, "chat_id", card)
|
||||
|
||||
# Wait for card callback or text reply y/n
|
||||
loop = asyncio.get_running_loop()
|
||||
self._pending_approval = loop.create_future()
|
||||
try:
|
||||
@@ -312,18 +375,14 @@ class SDKSession:
|
||||
except asyncio.TimeoutError:
|
||||
approved = False
|
||||
from bot.feishu import send_markdown
|
||||
|
||||
await send_markdown(self.chat_id, "chat_id", "⏰ 审批超时,已自动拒绝。")
|
||||
finally:
|
||||
self._pending_approval_desc = ""
|
||||
|
||||
from agent.audit import log_permission_decision
|
||||
|
||||
log_permission_decision(
|
||||
conv_id=self.conv_id,
|
||||
tool_name=tool_name,
|
||||
tool_input=input_data,
|
||||
approved=approved,
|
||||
conv_id=self.conv_id, tool_name=tool_name,
|
||||
tool_input=input_data, approved=approved,
|
||||
)
|
||||
if approved:
|
||||
return PermissionResultAllow()
|
||||
|
||||
Reference in New Issue
Block a user