From 9e15925cf4d73767d5a3163116b5f7f8eeedee29 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Tue, 12 May 2026 18:36:03 +0800 Subject: [PATCH] refactor(agent): remove ask_user tool The ask_user tool used AskUserInterrupt(BaseException) for mid-turn blocking, creating heavy coupling across runner, loop, and session management. The model now asks questions naturally in response text, the turn ends normally, and the user's next message starts a new turn with session history providing continuity. Removed: - nanobot/agent/tools/ask.py (tool, interrupt, helpers) - tests/agent/test_ask_user.py - webui/src/components/thread/AskUserPrompt.tsx - AskUserInterrupt handling in runner.py - Dual-path message building in loop.py - Pending ask detection via history scanning - button_prompt/buttons emission in WebSocket channel - ask_user references in Slack channel docstrings Preserved (MessageTool uses these independently): - OutboundMessage.buttons field - Channel button rendering (Telegram, Slack, WebSocket) --- nanobot/agent/loop.py | 44 +--- nanobot/agent/runner.py | 35 +-- nanobot/agent/tools/ask.py | 136 ---------- nanobot/channels/slack.py | 6 +- nanobot/channels/websocket.py | 13 - nanobot/skills/update-setup/SKILL.md | 8 +- tests/agent/test_ask_user.py | 241 ------------------ tests/channels/test_slack_channel.py | 4 +- tests/channels/test_websocket_channel.py | 4 +- tests/tools/test_tool_loader.py | 2 +- webui/src/components/thread/AskUserPrompt.tsx | 108 -------- webui/src/components/thread/ThreadShell.tsx | 23 -- webui/src/hooks/useNanobotStream.ts | 3 +- webui/src/lib/types.ts | 5 - webui/src/tests/thread-shell.test.tsx | 42 --- webui/src/tests/useNanobotStream.test.tsx | 23 -- 16 files changed, 24 insertions(+), 673 deletions(-) delete mode 100644 nanobot/agent/tools/ask.py delete mode 100644 tests/agent/test_ask_user.py delete mode 100644 webui/src/components/thread/AskUserPrompt.tsx diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index c73013379..476a2caf2 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -22,12 +22,6 @@ from nanobot.agent.memory import Consolidator, Dream from nanobot.agent import model_presets as preset_helpers from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN, AgentRunner, AgentRunSpec from nanobot.agent.subagent import SubagentManager -from nanobot.agent.tools.ask import ( - ask_user_options_from_messages, - ask_user_outbound, - ask_user_tool_result_messages, - pending_ask_user_id, -) from nanobot.agent.tools.file_state import FileStateStore, bind_file_states, reset_file_states from nanobot.agent.tools.message import MessageTool from nanobot.agent.tools.registry import ToolRegistry @@ -693,7 +687,6 @@ class AgentLoop: self, msg: InboundMessage, session: Session, - pending_ask_id: str | None, ) -> bool: """Persist the triggering user message before the turn starts. @@ -701,7 +694,7 @@ class AgentLoop: """ media_paths = [p for p in (msg.media or []) if isinstance(p, str) and p] has_text = isinstance(msg.content, str) and msg.content.strip() - if not pending_ask_id and (has_text or media_paths): + if has_text or media_paths: extra: dict[str, Any] = {"media": list(media_paths)} if media_paths else {} text = msg.content if isinstance(msg.content, str) else "" session.add_message("user", text, **extra) @@ -715,21 +708,9 @@ class AgentLoop: msg: InboundMessage, session: Session, history: list[dict[str, Any]], - pending_ask_id: str | None, pending_summary: str | None, ) -> list[dict[str, Any]]: """Build the initial message list for the LLM turn.""" - if pending_ask_id: - system_prompt = self.context.build_system_prompt( - channel=msg.channel, - session_summary=pending_summary, - ) - return ask_user_tool_result_messages( - system_prompt, - history, - pending_ask_id, - image_generation_prompt(msg.content, msg.metadata), - ) return self.context.build_messages( history=history, current_message=image_generation_prompt(msg.content, msg.metadata), @@ -1237,12 +1218,7 @@ class AgentLoop: replay_max_messages=self._max_messages, ) ) - options = ask_user_options_from_messages(all_msgs) if stop_reason == "ask_user" else [] - content, buttons = ask_user_outbound( - final_content or "Background task completed.", - options, - channel, - ) + content = final_content or "Background task completed." outbound_metadata: dict[str, Any] = {} if channel == "slack" and key.startswith("slack:") and key.count(":") >= 2: outbound_metadata["slack"] = {"thread_ts": key.split(":", 2)[2]} @@ -1252,7 +1228,6 @@ class AgentLoop: channel=channel, chat_id=chat_id, content=content, - buttons=buttons, metadata=outbound_metadata, ) @@ -1365,21 +1340,15 @@ class AgentLoop: logger.info("Response to {}:{}: {}", msg.channel, msg.sender_id, preview) meta = dict(msg.metadata or {}) - content, buttons = ask_user_outbound( - final_content, - ask_user_options_from_messages(all_msgs) if stop_reason == "ask_user" else [], - msg.channel, - ) - if on_stream is not None and stop_reason not in {"ask_user", "error", "tool_error"}: + if on_stream is not None and stop_reason not in {"error", "tool_error"}: meta["_streamed"] = True return OutboundMessage( channel=msg.channel, chat_id=msg.chat_id, - content=content, + content=final_content, media=generated_media, metadata=meta, - buttons=buttons, ) async def _state_restore(self, ctx: TurnContext) -> TurnState: @@ -1446,12 +1415,11 @@ class AgentLoop: } ctx.history = ctx.session.get_history(**_hist_kwargs) - pending_ask_id = pending_ask_user_id(ctx.history) ctx.initial_messages = self._build_initial_messages( - ctx.msg, ctx.session, ctx.history, pending_ask_id, ctx.pending_summary + ctx.msg, ctx.session, ctx.history, ctx.pending_summary ) ctx.user_persisted_early = self._persist_user_message_early( - ctx.msg, ctx.session, pending_ask_id + ctx.msg, ctx.session ) if ctx.on_progress is None: diff --git a/nanobot/agent/runner.py b/nanobot/agent/runner.py index 7fe92ad51..9ea0d26de 100644 --- a/nanobot/agent/runner.py +++ b/nanobot/agent/runner.py @@ -13,7 +13,6 @@ from typing import Any from loguru import logger from nanobot.agent.hook import AgentHook, AgentHookContext -from nanobot.agent.tools.ask import AskUserInterrupt from nanobot.agent.tools.registry import ToolRegistry from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest from nanobot.utils.helpers import ( @@ -283,22 +282,18 @@ class AgentRunner: self._accumulate_usage(usage, raw_usage) if response.should_execute_tools: - tool_calls = list(response.tool_calls) - ask_index = next((i for i, tc in enumerate(tool_calls) if tc.name == "ask_user"), None) - if ask_index is not None: - tool_calls = tool_calls[: ask_index + 1] - context.tool_calls = list(tool_calls) + context.tool_calls = list(response.tool_calls) if hook.wants_streaming(): await hook.on_stream_end(context, resuming=True) assistant_message = build_assistant_message( response.content or "", - tool_calls=[tc.to_openai_tool_call() for tc in tool_calls], + tool_calls=[tc.to_openai_tool_call() for tc in response.tool_calls], reasoning_content=response.reasoning_content, thinking_blocks=response.thinking_blocks, ) messages.append(assistant_message) - tools_used.extend(tc.name for tc in tool_calls) + tools_used.extend(tc.name for tc in response.tool_calls) await self._emit_checkpoint( spec, { @@ -307,7 +302,7 @@ class AgentRunner: "model": spec.model, "assistant_message": assistant_message, "completed_tool_results": [], - "pending_tool_calls": [tc.to_openai_tool_call() for tc in tool_calls], + "pending_tool_calls": [tc.to_openai_tool_call() for tc in response.tool_calls], }, ) @@ -315,7 +310,7 @@ class AgentRunner: results, new_events, fatal_error = await self._execute_tools( spec, - tool_calls, + response.tool_calls, external_lookup_counts, workspace_violation_counts, ) @@ -323,9 +318,7 @@ class AgentRunner: context.tool_results = list(results) context.tool_events = list(new_events) completed_tool_results: list[dict[str, Any]] = [] - for tool_call, result in zip(tool_calls, results): - if isinstance(fatal_error, AskUserInterrupt) and tool_call.name == "ask_user": - continue + for tool_call, result in zip(response.tool_calls, results): tool_message = { "role": "tool", "tool_call_id": tool_call.id, @@ -340,15 +333,6 @@ class AgentRunner: messages.append(tool_message) completed_tool_results.append(tool_message) if fatal_error is not None: - if isinstance(fatal_error, AskUserInterrupt): - final_content = fatal_error.question - stop_reason = "ask_user" - context.final_content = final_content - context.stop_reason = stop_reason - if hook.wants_streaming(): - await hook.on_stream_end(context, resuming=False) - await hook.after_iteration(context) - break error = f"Error: {type(fatal_error).__name__}: {fatal_error}" final_content = error stop_reason = "tool_error" @@ -724,10 +708,6 @@ class AgentRunner: ) tool_results.append(result) batch_results.append(result) - if isinstance(result[2], AskUserInterrupt): - break - if any(isinstance(error, AskUserInterrupt) for _, _, error in batch_results): - break results: list[Any] = [] events: list[dict[str, str]] = [] @@ -799,9 +779,6 @@ class AgentRunner: "status": "error", "detail": str(exc), } - if isinstance(exc, AskUserInterrupt): - event["status"] = "waiting" - return "", event, exc payload = f"Error: {type(exc).__name__}: {exc}" handled = self._classify_violation( raw_text=str(exc), diff --git a/nanobot/agent/tools/ask.py b/nanobot/agent/tools/ask.py deleted file mode 100644 index db8c83a84..000000000 --- a/nanobot/agent/tools/ask.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Tool for pausing a turn until the user answers.""" - -import json -from typing import Any - -from nanobot.agent.tools.base import Tool, tool_parameters -from nanobot.agent.tools.schema import ArraySchema, StringSchema, tool_parameters_schema - -STRUCTURED_BUTTON_CHANNELS = frozenset({"telegram", "websocket"}) - - -class AskUserInterrupt(BaseException): - """Internal signal: the runner should stop and wait for user input.""" - - def __init__(self, question: str, options: list[str] | None = None) -> None: - self.question = question - self.options = [str(option) for option in (options or []) if str(option)] - super().__init__(question) - - -@tool_parameters( - tool_parameters_schema( - question=StringSchema( - "The question to ask before continuing. Use this only when the task needs the user's answer." - ), - options=ArraySchema( - StringSchema("A possible answer label"), - description="Optional choices. The user may still reply with free text.", - ), - required=["question"], - ) -) -class AskUserTool(Tool): - """Ask the user a blocking question.""" - - @property - def name(self) -> str: - return "ask_user" - - @property - def description(self) -> str: - return ( - "Pause and ask the user a question when their answer is required to continue. " - "Use options for likely answers; the user's reply, typed or selected, is returned as the tool result. " - "For non-blocking notifications or buttons, use the message tool instead." - ) - - @property - def exclusive(self) -> bool: - return True - - async def execute(self, question: str, options: list[str] | None = None, **_: Any) -> Any: - raise AskUserInterrupt(question=question, options=options) - - -def _tool_call_name(tool_call: dict[str, Any]) -> str: - function = tool_call.get("function") - if isinstance(function, dict) and isinstance(function.get("name"), str): - return function["name"] - name = tool_call.get("name") - return name if isinstance(name, str) else "" - - -def _tool_call_arguments(tool_call: dict[str, Any]) -> dict[str, Any]: - function = tool_call.get("function") - raw = function.get("arguments") if isinstance(function, dict) else tool_call.get("arguments") - if isinstance(raw, dict): - return raw - if isinstance(raw, str): - try: - parsed = json.loads(raw) - except json.JSONDecodeError: - return {} - return parsed if isinstance(parsed, dict) else {} - return {} - - -def pending_ask_user_id(history: list[dict[str, Any]]) -> str | None: - pending: dict[str, str] = {} - for message in history: - if message.get("role") == "assistant": - for tool_call in message.get("tool_calls") or []: - if isinstance(tool_call, dict) and isinstance(tool_call.get("id"), str): - pending[tool_call["id"]] = _tool_call_name(tool_call) - elif message.get("role") == "tool": - tool_call_id = message.get("tool_call_id") - if isinstance(tool_call_id, str): - pending.pop(tool_call_id, None) - for tool_call_id, name in reversed(pending.items()): - if name == "ask_user": - return tool_call_id - return None - - -def ask_user_tool_result_messages( - system_prompt: str, - history: list[dict[str, Any]], - tool_call_id: str, - content: str, -) -> list[dict[str, Any]]: - return [ - {"role": "system", "content": system_prompt}, - *history, - { - "role": "tool", - "tool_call_id": tool_call_id, - "name": "ask_user", - "content": content, - }, - ] - - -def ask_user_options_from_messages(messages: list[dict[str, Any]]) -> list[str]: - for message in reversed(messages): - if message.get("role") != "assistant": - continue - for tool_call in reversed(message.get("tool_calls") or []): - if not isinstance(tool_call, dict) or _tool_call_name(tool_call) != "ask_user": - continue - options = _tool_call_arguments(tool_call).get("options") - if isinstance(options, list): - return [str(option) for option in options if isinstance(option, str)] - return [] - - -def ask_user_outbound( - content: str | None, - options: list[str], - channel: str, -) -> tuple[str | None, list[list[str]]]: - if not options: - return content, [] - if channel in STRUCTURED_BUTTON_CHANNELS: - return content, [options] - option_text = "\n".join(f"{index}. {option}" for index, option in enumerate(options, 1)) - return f"{content}\n\n{option_text}" if content else option_text, [] diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index dc8899861..be3172bff 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -471,7 +471,7 @@ class SlackChannel(BaseChannel): return preview.startswith(_HTML_DOWNLOAD_PREFIXES) async def _on_block_action(self, client: SocketModeClient, req: SocketModeRequest) -> None: - """Handle button clicks from ask_user blocks.""" + """Handle button clicks from inline action buttons.""" await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id)) payload = req.payload or {} actions = payload.get("actions") or [] @@ -568,7 +568,7 @@ class SlackChannel(BaseChannel): @staticmethod def _build_button_blocks(text: str, buttons: list[list[str]]) -> list[dict[str, Any]]: - """Build Slack Block Kit blocks with action buttons for ask_user choices.""" + """Build Slack Block Kit blocks with action buttons.""" blocks: list[dict[str, Any]] = [ {"type": "section", "text": {"type": "mrkdwn", "text": text[:3000]}}, ] @@ -579,7 +579,7 @@ class SlackChannel(BaseChannel): "type": "button", "text": {"type": "plain_text", "text": label[:75]}, "value": label[:75], - "action_id": f"ask_user_{label[:50]}", + "action_id": f"btn_{label[:50]}", }) if elements: blocks.append({"type": "actions", "elements": elements[:25]}) diff --git a/nanobot/channels/websocket.py b/nanobot/channels/websocket.py index 86a1e9654..76ca513d0 100644 --- a/nanobot/channels/websocket.py +++ b/nanobot/channels/websocket.py @@ -55,14 +55,6 @@ def _normalize_config_path(path: str) -> str: return _strip_trailing_slash(path) -def _append_buttons_as_text(text: str, buttons: list[list[str]]) -> str: - labels = [label for row in buttons for label in row if label] - if not labels: - return text - fallback = "\n".join(f"{index}. {label}" for index, label in enumerate(labels, 1)) - return f"{text}\n\n{fallback}" if text else fallback - - class WebSocketConfig(Base): """WebSocket server channel configuration. @@ -1468,16 +1460,11 @@ class WebSocketChannel(BaseChannel): await self.send_session_updated(msg.chat_id) return text = msg.content - if msg.buttons: - text = _append_buttons_as_text(text, msg.buttons) payload: dict[str, Any] = { "event": "message", "chat_id": msg.chat_id, "text": text, } - if msg.buttons: - payload["buttons"] = msg.buttons - payload["button_prompt"] = msg.content if msg.media: payload["media"] = msg.media urls: list[dict[str, str]] = [] diff --git a/nanobot/skills/update-setup/SKILL.md b/nanobot/skills/update-setup/SKILL.md index 7e9d5cc60..0838168f5 100644 --- a/nanobot/skills/update-setup/SKILL.md +++ b/nanobot/skills/update-setup/SKILL.md @@ -11,7 +11,7 @@ Generate a personalized upgrade skill for this workspace. Use `read_file` to check if `skills/update/SKILL.md` already exists in the workspace. -If it exists, use `ask_user` to ask: "An upgrade skill already exists. Reconfigure?" with options ["yes", "no"]. If no, stop here. +If it exists, ask the user: "An upgrade skill already exists. Reconfigure?" Wait for the user's reply. If no, stop here. ## Step 2: Current Version and Install Clues @@ -38,9 +38,9 @@ answer or confirmation, not from inference alone. If you cannot get a clear answer, stop and ask the user to rerun this setup when they know how nanobot was installed. -Use `ask_user` for the questions below, one question per call. If `ask_user` is -not available or cannot collect the answer, ask in normal chat and stop without -writing the skill. +Ask the user the questions below, one at a time, in your response text. Wait for +the user's reply before proceeding to the next question. If you cannot get a clear +answer, stop without writing the skill. **Question 1 — Install method:** diff --git a/tests/agent/test_ask_user.py b/tests/agent/test_ask_user.py deleted file mode 100644 index a192ee4a6..000000000 --- a/tests/agent/test_ask_user.py +++ /dev/null @@ -1,241 +0,0 @@ -import asyncio -from unittest.mock import MagicMock - -import pytest - -from nanobot.agent.loop import AgentLoop -from nanobot.agent.runner import AgentRunner, AgentRunSpec -from nanobot.agent.tools.ask import AskUserInterrupt, AskUserTool -from nanobot.agent.tools.base import Tool, tool_parameters -from nanobot.agent.tools.registry import ToolRegistry -from nanobot.agent.tools.schema import tool_parameters_schema -from nanobot.bus.events import InboundMessage -from nanobot.bus.queue import MessageBus -from nanobot.providers.base import GenerationSettings, LLMResponse, ToolCallRequest - - -def _make_provider(chat_with_retry): - async def chat_stream_with_retry(**kwargs): - kwargs.pop("on_content_delta", None) - return await chat_with_retry(**kwargs) - - provider = MagicMock() - provider.get_default_model.return_value = "test-model" - provider.generation = GenerationSettings() - provider.chat_with_retry = chat_with_retry - provider.chat_stream_with_retry = chat_stream_with_retry - return provider - - -def test_ask_user_tool_schema_and_interrupt(): - tool = AskUserTool() - schema = tool.to_schema()["function"] - - assert schema["name"] == "ask_user" - assert "question" in schema["parameters"]["required"] - assert schema["parameters"]["properties"]["options"]["type"] == "array" - - with pytest.raises(AskUserInterrupt) as exc: - asyncio.run(tool.execute("Continue?", options=["Yes", "No"])) - - assert exc.value.question == "Continue?" - assert exc.value.options == ["Yes", "No"] - - -@pytest.mark.asyncio -async def test_runner_pauses_on_ask_user_without_executing_later_tools(): - @tool_parameters(tool_parameters_schema(required=[])) - class LaterTool(Tool): - called = False - - @property - def name(self) -> str: - return "later" - - @property - def description(self) -> str: - return "Should not run after ask_user pauses the turn." - - async def execute(self, **kwargs): - self.called = True - return "later result" - - async def chat_with_retry(**kwargs): - return LLMResponse( - content="", - finish_reason="tool_calls", - tool_calls=[ - ToolCallRequest( - id="call_ask", - name="ask_user", - arguments={"question": "Install this package?", "options": ["Yes", "No"]}, - ), - ToolCallRequest(id="call_later", name="later", arguments={}), - ], - ) - - later = LaterTool() - tools = ToolRegistry() - tools.register(AskUserTool()) - tools.register(later) - - result = await AgentRunner(_make_provider(chat_with_retry)).run(AgentRunSpec( - initial_messages=[{"role": "user", "content": "continue"}], - tools=tools, - model="test-model", - max_iterations=3, - max_tool_result_chars=16_000, - concurrent_tools=True, - )) - - assert result.stop_reason == "ask_user" - assert result.final_content == "Install this package?" - assert "ask_user" in result.tools_used - assert later.called is False - assert result.messages[-1]["role"] == "assistant" - tool_calls = result.messages[-1]["tool_calls"] - assert [tool_call["function"]["name"] for tool_call in tool_calls] == ["ask_user"] - assert not any(message.get("name") == "ask_user" for message in result.messages) - - -@pytest.mark.asyncio -async def test_ask_user_text_fallback_resumes_with_next_message(tmp_path): - seen_messages: list[list[dict]] = [] - - async def chat_with_retry(**kwargs): - seen_messages.append(kwargs["messages"]) - if len(seen_messages) == 1: - return LLMResponse( - content="", - finish_reason="tool_calls", - tool_calls=[ - ToolCallRequest( - id="call_ask", - name="ask_user", - arguments={ - "question": "Install the optional package?", - "options": ["Install", "Skip"], - }, - ) - ], - ) - return LLMResponse(content="Skipped install.", usage={}) - - loop = AgentLoop( - bus=MessageBus(), - provider=_make_provider(chat_with_retry), - workspace=tmp_path, - model="test-model", - ) - - async def on_stream(delta: str) -> None: - pass - - async def on_stream_end(**kwargs) -> None: - pass - - first = await loop._process_message( - InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="set it up"), - on_stream=on_stream, - on_stream_end=on_stream_end, - ) - - assert first is not None - assert first.content == "Install the optional package?\n\n1. Install\n2. Skip" - assert first.buttons == [] - assert "_streamed" not in first.metadata - - session = loop.sessions.get_or_create("cli:direct") - assert any(message.get("role") == "assistant" and message.get("tool_calls") for message in session.messages) - assert not any(message.get("role") == "tool" and message.get("name") == "ask_user" for message in session.messages) - - second = await loop._process_message( - InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="Skip") - ) - - assert second is not None - assert second.content == "Skipped install." - assert any( - message.get("role") == "tool" - and message.get("name") == "ask_user" - and message.get("content") == "Skip" - for message in seen_messages[-1] - ) - assert not any( - message.get("role") == "user" and message.get("content") == "Skip" - for message in session.messages - ) - assert any( - message.get("role") == "tool" - and message.get("name") == "ask_user" - and message.get("content") == "Skip" - for message in session.messages - ) - - -@pytest.mark.asyncio -async def test_ask_user_keeps_buttons_for_telegram(tmp_path): - async def chat_with_retry(**kwargs): - return LLMResponse( - content="", - finish_reason="tool_calls", - tool_calls=[ - ToolCallRequest( - id="call_ask", - name="ask_user", - arguments={ - "question": "Install the optional package?", - "options": ["Install", "Skip"], - }, - ) - ], - ) - - loop = AgentLoop( - bus=MessageBus(), - provider=_make_provider(chat_with_retry), - workspace=tmp_path, - model="test-model", - ) - - response = await loop._process_message( - InboundMessage(channel="telegram", sender_id="user", chat_id="123", content="set it up") - ) - - assert response is not None - assert response.content == "Install the optional package?" - assert response.buttons == [["Install", "Skip"]] - - -@pytest.mark.asyncio -async def test_ask_user_keeps_buttons_for_websocket(tmp_path): - async def chat_with_retry(**kwargs): - return LLMResponse( - content="", - finish_reason="tool_calls", - tool_calls=[ - ToolCallRequest( - id="call_ask", - name="ask_user", - arguments={ - "question": "Install the optional package?", - "options": ["Install", "Skip"], - }, - ) - ], - ) - - loop = AgentLoop( - bus=MessageBus(), - provider=_make_provider(chat_with_retry), - workspace=tmp_path, - model="test-model", - ) - - response = await loop._process_message( - InboundMessage(channel="websocket", sender_id="user", chat_id="123", content="set it up") - ) - - assert response is not None - assert response.content == "Install the optional package?" - assert response.buttons == [["Install", "Skip"]] diff --git a/tests/channels/test_slack_channel.py b/tests/channels/test_slack_channel.py index 630685eed..d0f41766a 100644 --- a/tests/channels/test_slack_channel.py +++ b/tests/channels/test_slack_channel.py @@ -234,13 +234,13 @@ async def test_send_renders_buttons_on_last_message_chunk() -> None: "type": "button", "text": {"type": "plain_text", "text": "Yes"}, "value": "Yes", - "action_id": "ask_user_Yes", + "action_id": "btn_Yes", }, { "type": "button", "text": {"type": "plain_text", "text": "No"}, "value": "No", - "action_id": "ask_user_No", + "action_id": "btn_No", }, ], } diff --git a/tests/channels/test_websocket_channel.py b/tests/channels/test_websocket_channel.py index af144dbf7..92b61f7d6 100644 --- a/tests/channels/test_websocket_channel.py +++ b/tests/channels/test_websocket_channel.py @@ -224,11 +224,9 @@ async def test_send_delivers_json_message_with_media_and_reply() -> None: payload = json.loads(mock_ws.send.call_args[0][0]) assert payload["event"] == "message" assert payload["chat_id"] == "chat-1" - assert payload["text"] == "hello\n\n1. Yes\n2. No" - assert payload["button_prompt"] == "hello" + assert payload["text"] == "hello" assert payload["reply_to"] == "m1" assert payload["media"] == ["/tmp/a.png"] - assert payload["buttons"] == [["Yes", "No"]] @pytest.mark.asyncio diff --git a/tests/tools/test_tool_loader.py b/tests/tools/test_tool_loader.py index 60ad8057b..fa33b140b 100644 --- a/tests/tools/test_tool_loader.py +++ b/tests/tools/test_tool_loader.py @@ -405,7 +405,7 @@ def test_loader_registers_same_tools_as_old_hardcoded(): registered = loader.load(ctx, registry) expected = { - "ask_user", "read_file", "write_file", "edit_file", "list_dir", + "read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "notebook_edit", "exec", "web_search", "web_fetch", "message", "spawn", "cron", } diff --git a/webui/src/components/thread/AskUserPrompt.tsx b/webui/src/components/thread/AskUserPrompt.tsx deleted file mode 100644 index 4de76307c..000000000 --- a/webui/src/components/thread/AskUserPrompt.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { MessageSquareText } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; - -interface AskUserPromptProps { - question: string; - buttons: string[][]; - onAnswer: (answer: string) => void; -} - -export function AskUserPrompt({ - question, - buttons, - onAnswer, -}: AskUserPromptProps) { - const [customOpen, setCustomOpen] = useState(false); - const [custom, setCustom] = useState(""); - const inputRef = useRef(null); - const options = buttons.flat().filter(Boolean); - - useEffect(() => { - if (customOpen) { - inputRef.current?.focus(); - } - }, [customOpen]); - - const submitCustom = useCallback(() => { - const answer = custom.trim(); - if (!answer) return; - onAnswer(answer); - setCustom(""); - setCustomOpen(false); - }, [custom, onAnswer]); - - if (options.length === 0) return null; - - return ( -
-
-
- -
-

- {question} -

-
- -
- {options.map((option) => ( - - ))} - -
- - {customOpen ? ( -
-