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.
This commit is contained in:
LIghtJUNction
2026-03-23 03:11:05 +08:00
parent 47aa6ea2cd
commit 1c7085d650
7 changed files with 84 additions and 69 deletions

View File

@@ -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)

View File

@@ -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"},
},
},
},

View File

@@ -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",
]

View File

@@ -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)

View File

@@ -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:

View File

@@ -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.")

View File

@@ -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")