feat(channels): enable tool hints by default

This commit is contained in:
chengyongru
2026-07-26 21:22:12 +08:00
committed by chengyongru
parent ee93725e83
commit d576804f23
8 changed files with 27 additions and 16 deletions
+2 -2
View File
@@ -618,7 +618,7 @@ async def send(self, msg: OutboundMessage) -> None:
await self._send_message(msg.chat_id, msg.content, media=msg.media) await self._send_message(msg.chat_id, msg.content, media=msg.media)
``` ```
Tool hints are off by default for most channels. Users can enable them globally or per channel: Tool hints are on by default. Users can disable them globally or per channel:
```json ```json
{ {
@@ -626,7 +626,7 @@ Tool hints are off by default for most channels. Users can enable them globally
"sendToolHints": true, "sendToolHints": true,
"webhook": { "webhook": {
"enabled": true, "enabled": true,
"sendToolHints": true "sendToolHints": false
} }
} }
} }
+5 -4
View File
@@ -1555,7 +1555,7 @@ Global settings that apply to all channels. Configure under the `channels` secti
{ {
"channels": { "channels": {
"sendProgress": true, "sendProgress": true,
"sendToolHints": false, "sendToolHints": true,
"extractDocumentText": true, "extractDocumentText": true,
"sendMaxRetries": 3, "sendMaxRetries": 3,
"telegram": { "telegram": {
@@ -1568,7 +1568,7 @@ Global settings that apply to all channels. Configure under the `channels` secti
| Setting | Default | Description | | Setting | Default | Description |
|---------|---------|-------------| |---------|---------|-------------|
| `sendProgress` | `true` | Stream agent's text progress to the channel | | `sendProgress` | `true` | Stream agent's text progress to the channel |
| `sendToolHints` | `false` | Stream tool-call hints (e.g. `read_file("…")`) | | `sendToolHints` | `true` | Stream tool-call hints (e.g. `read_file("…")`) |
| `showReasoning` | `true` | Allow channels to surface model reasoning/thinking content (DeepSeek-R1 `reasoning_content`, Anthropic `thinking_blocks`, inline `<think>` tags). Reasoning flows as a dedicated stream with `_reasoning_delta` / `_reasoning_end` markers — channels override `send_reasoning_delta` / `send_reasoning_end` to render in-place updates. Even with `true`, channels without those overrides stay no-op silently. Currently surfaced on CLI and WebSocket/WebUI (italic shimmer header, auto-collapses after the stream ends); Telegram / Slack / Discord / Feishu / WeChat / Matrix / Mattermost keep the base no-op until their bubble UI is adapted. Independent of `sendProgress`. | | `showReasoning` | `true` | Allow channels to surface model reasoning/thinking content (DeepSeek-R1 `reasoning_content`, Anthropic `thinking_blocks`, inline `<think>` tags). Reasoning flows as a dedicated stream with `_reasoning_delta` / `_reasoning_end` markers — channels override `send_reasoning_delta` / `send_reasoning_end` to render in-place updates. Even with `true`, channels without those overrides stay no-op silently. Currently surfaced on CLI and WebSocket/WebUI (italic shimmer header, auto-collapses after the stream ends); Telegram / Slack / Discord / Feishu / WeChat / Matrix / Mattermost keep the base no-op until their bubble UI is adapted. Independent of `sendProgress`. |
| `extractDocumentText` | `true` | Extract supported document/text attachments into the model prompt. PDF, DOCX, XLSX, and PPTX readers are included in the standard installation. Set to `false` to keep document content out of the prompt and include attachment path references instead. | | `extractDocumentText` | `true` | Extract supported document/text attachments into the model prompt. PDF, DOCX, XLSX, and PPTX readers are included in the standard installation. Set to `false` to keep document content out of the prompt and include attachment path references instead. |
| `sendMaxRetries` | `3` | Max delivery attempts per outbound message, including the initial send (0-10 configured, minimum 1 actual attempt) | | `sendMaxRetries` | `3` | Max delivery attempts per outbound message, including the initial send (0-10 configured, minimum 1 actual attempt) |
@@ -1581,10 +1581,11 @@ Global settings that apply to all channels. Configure under the `channels` secti
{ {
"channels": { "channels": {
"sendProgress": true, "sendProgress": true,
"sendToolHints": false, "sendToolHints": true,
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"sendProgress": false "sendProgress": false,
"sendToolHints": false
}, },
"websocket": { "websocket": {
"enabled": true, "enabled": true,
+1 -1
View File
@@ -29,7 +29,7 @@ class BaseChannel(ABC):
name: str = "base" name: str = "base"
display_name: str = "Base" display_name: str = "Base"
send_progress: bool = True send_progress: bool = True
send_tool_hints: bool = False send_tool_hints: bool = True
show_reasoning: bool = True show_reasoning: bool = True
def __init__(self, config: Any, bus: MessageBus): def __init__(self, config: Any, bus: MessageBus):
+1 -1
View File
@@ -56,7 +56,7 @@ class MattermostConfig(Base):
react_emoji: str = "eyes" react_emoji: str = "eyes"
done_emoji: str = "white_check_mark" done_emoji: str = "white_check_mark"
send_progress: bool = True send_progress: bool = True
send_tool_hints: bool = False send_tool_hints: bool = True
dm: MattermostDMConfig = Field(default_factory=MattermostDMConfig) dm: MattermostDMConfig = Field(default_factory=MattermostDMConfig)
@@ -119,6 +119,7 @@ def test_config_defaults():
assert config.token == "" assert config.token == ""
assert config.streaming is True assert config.streaming is True
assert config.streaming_max_chars == 16000 assert config.streaming_max_chars == 16000
assert config.send_tool_hints is True
assert config.dm.enabled is True assert config.dm.enabled is True
assert config.dm.policy == "open" assert config.dm.policy == "open"
assert config.reply_in_thread is True assert config.reply_in_thread is True
@@ -131,6 +132,7 @@ def test_config_camelcase_aliases():
"allowFromMatchMode": "username", "allowFromMatchMode": "username",
"streamingMaxChars": 8000, "streamingMaxChars": 8000,
"replyInThread": False, "replyInThread": False,
"sendToolHints": False,
} }
config = MattermostConfig.model_validate(raw) config = MattermostConfig.model_validate(raw)
assert config.server_url == "https://mm.example.com" assert config.server_url == "https://mm.example.com"
@@ -138,11 +140,13 @@ def test_config_camelcase_aliases():
assert config.allow_from_match_mode == "username" assert config.allow_from_match_mode == "username"
assert config.streaming_max_chars == 8000 assert config.streaming_max_chars == 8000
assert config.reply_in_thread is False assert config.reply_in_thread is False
assert config.send_tool_hints is False
def test_config_default_config_classmethod(): def test_config_default_config_classmethod():
d = MattermostChannel.default_config() d = MattermostChannel.default_config()
assert d["enabled"] is False assert d["enabled"] is False
assert d["sendToolHints"] is True
assert d["serverUrl"] == "" assert d["serverUrl"] == ""
assert d["token"] == "" assert d["token"] == ""
+1 -1
View File
@@ -30,7 +30,7 @@ class ChannelsConfig(Base):
model_config = ConfigDict(extra="allow") model_config = ConfigDict(extra="allow")
send_progress: bool = True # stream agent's text progress to the channel send_progress: bool = True # stream agent's text progress to the channel
send_tool_hints: bool = False # stream tool-call hints (e.g. read_file("…")) send_tool_hints: bool = True # stream tool-call hints (e.g. read_file("…"))
show_reasoning: bool = True # surface model reasoning when channel implements it show_reasoning: bool = True # surface model reasoning when channel implements it
extract_document_text: bool = True # extract text from document attachments before sending to the model extract_document_text: bool = True # extract text from document attachments before sending to the model
send_max_retries: int = Field(default=3, ge=0, le=10) # Max delivery attempts (initial send included) send_max_retries: int = Field(default=3, ge=0, le=10) # Max delivery attempts (initial send included)
@@ -74,7 +74,7 @@ def bus():
@pytest.fixture @pytest.fixture
def manager(config, bus): def manager(config, bus):
manager = ChannelManager(config, bus) manager = ChannelManager(config, bus)
manager.channels["mock"] = MockChannel({}, bus) manager.channels["mock"] = manager._build_channel("mock", MockChannel, {})
return manager return manager
@@ -284,14 +284,17 @@ class TestProgressFiltering:
def test_progress_visibility_uses_global_defaults(self, manager): def test_progress_visibility_uses_global_defaults(self, manager):
assert manager._should_send_progress("mock", tool_hint=False) is True assert manager._should_send_progress("mock", tool_hint=False) is True
assert manager._should_send_progress("mock", tool_hint=True) is False assert manager._should_send_progress("mock", tool_hint=True) is True
def test_progress_visibility_uses_channel_overrides(self, manager): def test_progress_visibility_uses_channel_overrides(self, manager, bus):
manager.channels["mock"].send_progress = False manager.channels["mock"] = manager._build_channel(
manager.channels["mock"].send_tool_hints = True "mock",
MockChannel,
{"sendProgress": False, "sendToolHints": False},
)
assert manager._should_send_progress("mock", tool_hint=False) is False assert manager._should_send_progress("mock", tool_hint=False) is False
assert manager._should_send_progress("mock", tool_hint=True) is True assert manager._should_send_progress("mock", tool_hint=True) is False
def test_progress_visibility_returns_false_for_missing_channel(self, manager): def test_progress_visibility_returns_false_for_missing_channel(self, manager):
assert manager._should_send_progress("nonexistent", tool_hint=False) is False assert manager._should_send_progress("nonexistent", tool_hint=False) is False
+4 -1
View File
@@ -269,9 +269,12 @@ def test_channels_config_has_no_per_channel_fields():
cfg = ChannelsConfig() cfg = ChannelsConfig()
assert not hasattr(cfg, "telegram") assert not hasattr(cfg, "telegram")
assert cfg.send_progress is True assert cfg.send_progress is True
assert cfg.send_tool_hints is False assert cfg.send_tool_hints is True
assert cfg.extract_document_text is True assert cfg.extract_document_text is True
opted_out = ChannelsConfig.model_validate({"sendToolHints": False})
assert opted_out.send_tool_hints is False
def test_channels_config_extract_document_text_accepts_camel_alias(): def test_channels_config_extract_document_text_accepts_camel_alias():
cfg = ChannelsConfig.model_validate({"extractDocumentText": False}) cfg = ChannelsConfig.model_validate({"extractDocumentText": False})