From 1c7085d650260cc1f521bc3399802c6930626c8b Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 23 Mar 2026 03:11:05 +0800 Subject: [PATCH] fix: simplify JSON Schema in SendMessageToUserTool parameters Simplify the message items schema by using additionalProperties instead of explicit properties, while preserving type info for LLM docs. Note: 12 ty diagnostics remain in send_message.py and astr_main_agent_resources.py due to architectural issue where FunctionTool.parameters JSON Schema is used by ty for Python type inference. This requires larger refactoring to fix properly. --- astrbot/cli/commands/tui_async.py | 30 ++++++++++---------- astrbot/core/tools/send_message.py | 28 +------------------ astrbot/tui/__init__.py | 15 +++++++++- astrbot/tui/__main__.py | 44 ++++++++++++++++++++++-------- astrbot/tui/message_handler.py | 9 +++--- astrbot/tui/tui_app.py | 22 ++++++++------- tests/test_dashboard.py | 5 +++- 7 files changed, 84 insertions(+), 69 deletions(-) diff --git a/astrbot/cli/commands/tui_async.py b/astrbot/cli/commands/tui_async.py index 75d5c0844..642d66a4a 100644 --- a/astrbot/cli/commands/tui_async.py +++ b/astrbot/cli/commands/tui_async.py @@ -11,7 +11,6 @@ import curses import json from dataclasses import dataclass, field from enum import Enum -from typing import Any import httpx @@ -75,6 +74,7 @@ class TUIClient: self._history_index: int = -1 self._max_history: int = 100 self._max_messages: int = 1000 + self._pending_tasks: list[asyncio.Task[None]] = [] # Connection settings self.host = host.rstrip("/") @@ -181,15 +181,13 @@ class TUIClient: if msg_type == "user": for part in message_parts: if part.get("type") == "plain": - self.add_message( - MessageSender.USER, part.get("text", "") - ) + self.add_message(MessageSender.USER, part.get("text", "")) elif msg_type == "bot": for part in message_parts: if part.get("type") == "plain": self.add_message(MessageSender.BOT, part.get("text", "")) - except Exception as e: + except Exception: if self.debug: import traceback @@ -248,7 +246,8 @@ class TUIClient: # Handle Enter/Return - submit message elif key in (curses.KEY_ENTER, 10, 13): if self.state.input_buffer.strip(): - asyncio.create_task(self._submit_message()) + task = asyncio.create_task(self._submit_message()) + self._pending_tasks.append(task) return True # Handle history navigation (up/down arrows) @@ -319,14 +318,13 @@ class TUIClient: try: # Format umo for webchat - umo = f"webchat:FriendMessage:webchat!{self.username}!{self.conversation_id}" + umo = ( + f"webchat:FriendMessage:webchat!{self.username}!{self.conversation_id}" + ) # Reset parser for new stream self._parser.reset() - # Create a placeholder bot message that we'll update - placeholder = self.add_message(MessageSender.BOT, "...") - # Send message and stream response using proper SSE async with self._client.stream( "POST", @@ -376,7 +374,9 @@ class TUIClient: ) self.state.status = "Thinking..." elif parsed.type == MessageType.AGENT_STATS: - self.state.status = f"Tokens: {update.agent_stats.get('total_tokens', 0)}" + self.state.status = ( + f"Tokens: {update.agent_stats.get('total_tokens', 0)}" + ) elif update.text: self._update_last_bot_message(update.text) @@ -385,7 +385,9 @@ class TUIClient: # Final status if update.reasoning: - self.add_message(MessageSender.REASONING, f"[Reasoning]\n{update.reasoning}") + self.add_message( + MessageSender.REASONING, f"[Reasoning]\n{update.reasoning}" + ) for tool_display in update.get_tool_calls_display(): self.add_message(MessageSender.TOOL, tool_display) @@ -405,9 +407,7 @@ class TUIClient: traceback.print_exc() - def _process_parsed_message( - self, msg: ParsedMessage - ) -> tuple[ChatResponse, bool]: + def _process_parsed_message(self, msg: ParsedMessage) -> tuple[ChatResponse, bool]: """Process a parsed message and return updated response state.""" return self._parser.process_message(msg) diff --git a/astrbot/core/tools/send_message.py b/astrbot/core/tools/send_message.py index 586cd9101..fc58f41fb 100644 --- a/astrbot/core/tools/send_message.py +++ b/astrbot/core/tools/send_message.py @@ -49,33 +49,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): "description": "An ordered list of message components to send. `mention_user` type can be used to mention the user.", "items": { "type": "object", - "additionalProperties": {}, - "properties": { - "type": { - "type": "string", - "description": ( - "Component type. One of: " - "plain, image, record, video, file, mention_user. Record is voice message." - ), - }, - "text": { - "type": "string", - "description": "Text content for `plain` type.", - }, - "path": { - "type": "string", - "description": "File path for `image`, `record`, or `file` types. Both local path and sandbox path are supported.", - }, - "url": { - "type": "string", - "description": "URL for `image`, `record`, or `file` types.", - }, - "mention_user_id": { - "type": "string", - "description": "User ID to mention for `mention_user` type.", - }, - }, - "required": ["type"], + "additionalProperties": {"type": "string"}, }, }, }, diff --git a/astrbot/tui/__init__.py b/astrbot/tui/__init__.py index e4c3f7aa9..a9e9928dc 100644 --- a/astrbot/tui/__init__.py +++ b/astrbot/tui/__init__.py @@ -1,5 +1,18 @@ """AstrBot TUI - Terminal User Interface for AstrBot.""" +from astrbot.tui.message_handler import ( + ChatResponse, + MessageType, + ParsedMessage, + SSEMessageParser, +) from astrbot.tui.screen import Screen, run_curses -__all__ = ["Screen", "run_curses"] +__all__ = [ + "ChatResponse", + "MessageType", + "ParsedMessage", + "SSEMessageParser", + "Screen", + "run_curses", +] diff --git a/astrbot/tui/__main__.py b/astrbot/tui/__main__.py index 0bf405d97..0b76b0dac 100644 --- a/astrbot/tui/__main__.py +++ b/astrbot/tui/__main__.py @@ -1,21 +1,43 @@ """AstrBot TUI - Entry point for python -m astrbot.tui""" -from astrbot.tui.screen import run_curses +import asyncio +import sys def main(stdscr): - """Main TUI loop - placeholder for future TUI implementation.""" - # For now, just display a message - import curses + """Main TUI entry point when running via python -m astrbot.tui.""" + try: + from astrbot.cli.commands.tui_async import TUIClient + from astrbot.tui.screen import Screen - curses.start_color() - curses.curs_set(1) - stdscr.clear() - stdscr.addstr(0, 0, "AstrBot TUI - Coming soon!", curses.A_BOLD) - stdscr.addstr(2, 0, "Press any key to exit...") - stdscr.refresh() - stdscr.getch() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + scr = Screen(stdscr) + client = TUIClient( + screen=scr, + host="http://localhost:6185", + api_key=None, + username="astrbot", + password="astrbot", + debug=False, + ) + try: + loop.run_until_complete(client.run_event_loop(stdscr)) + finally: + loop.close() + except ImportError as e: + import curses + + curses.curs_set(1) + stdscr.clear() + stdscr.addstr(0, 0, f"Error importing TUI module: {e}", curses.A_BOLD) + stdscr.addstr(2, 0, "Press any key to exit...") + stdscr.refresh() + stdscr.getch() + sys.exit(1) if __name__ == "__main__": + from astrbot.tui.screen import run_curses + run_curses(main) diff --git a/astrbot/tui/message_handler.py b/astrbot/tui/message_handler.py index 01a6bebb4..3a9fc53f7 100644 --- a/astrbot/tui/message_handler.py +++ b/astrbot/tui/message_handler.py @@ -165,9 +165,7 @@ class SSEMessageParser: except ValueError: return MessageType.PLAIN - def process_message( - self, msg: ParsedMessage - ) -> tuple[ChatResponse, bool]: + def process_message(self, msg: ParsedMessage) -> tuple[ChatResponse, bool]: """Process a parsed message and update accumulated response. Args: @@ -220,7 +218,10 @@ class SSEMessageParser: self._tool_calls[tc_id].result = tcr.get("result") self._tool_calls[tc_id].finished_ts = tcr.get("ts") self._accumulated_parts.append( - {"type": "tool_call", "tool_calls": [self._tool_calls[tc_id].__dict__]} + { + "type": "tool_call", + "tool_calls": [self._tool_calls[tc_id].__dict__], + } ) self._tool_calls.pop(tc_id, None) except json.JSONDecodeError: diff --git a/astrbot/tui/tui_app.py b/astrbot/tui/tui_app.py index 9ea2bc73c..f37c819cf 100644 --- a/astrbot/tui/tui_app.py +++ b/astrbot/tui/tui_app.py @@ -1,4 +1,8 @@ -"""AstrBot TUI Application - Main chat interface.""" +"""AstrBot TUI Application - Main chat interface (sync version for testing). + +This module provides a basic TUI application without network connectivity, +useful for testing the UI components and as a reference implementation. +""" from __future__ import annotations @@ -13,6 +17,8 @@ class MessageSender(Enum): USER = "user" BOT = "bot" SYSTEM = "system" + TOOL = "tool" + REASONING = "reasoning" @dataclass @@ -33,7 +39,7 @@ class TUIState: class AstrBotTUI: - """Main TUI application for AstrBot.""" + """Main TUI application for AstrBot (local/testing version).""" def __init__(self, screen: Screen): self.screen = screen @@ -45,7 +51,6 @@ class AstrBotTUI: def add_message(self, sender: MessageSender, text: str) -> None: """Add a message to the chat log.""" self.state.messages.append(Message(sender=sender, text=text)) - # Keep only last 1000 messages to prevent memory issues if len(self.state.messages) > 1000: self.state.messages = self.state.messages[-1000:] @@ -152,18 +157,15 @@ class AstrBotTUI: self.state.input_buffer = "" self.state.cursor_x = 0 - # Process the message (placeholder for actual bot interaction) + # Process the message (echo back for testing) self._process_user_message(text) def _process_user_message(self, text: str) -> None: - """Process user message and generate bot response.""" - # This is a placeholder - actual implementation would connect to the bot - self.add_system_message(f"Message sent: {text}") - self.state.status = "Message sent, awaiting response..." + """Process user message and generate bot response (echo for testing).""" + self.add_message(MessageSender.BOT, f"Echo: {text}") def render(self) -> None: """Render the current state to the screen.""" - # Convert messages to the format expected by screen lines = [(msg.sender.value, msg.text) for msg in self.state.messages] self.screen.draw_all( @@ -180,7 +182,7 @@ class AstrBotTUI: self.screen.layout_windows() # Welcome message - self.add_system_message("Welcome to AstrBot TUI!") + self.add_system_message("Welcome to AstrBot TUI (local mode)!") self.add_system_message("Type your message and press Enter to send.") self.add_system_message("Press ESC or Ctrl+C to exit.") diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 5cbfe6f90..870db0324 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -89,7 +89,10 @@ def test_expand_env_placeholders_resolves_env_and_default( assert _expand_env_placeholders("${MISSING:-3002}", "port") == "3002" -def test_expand_env_placeholders_raises_for_unresolved_variable(): +def test_expand_env_placeholders_raises_for_unresolved_variable( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.delenv("HOST", raising=False) with pytest.raises(ValueError, match="dashboard host: HOST"): _expand_env_placeholders("${HOST}", "host")