Compare commits

...

5 Commits

9 changed files with 150 additions and 38 deletions

View File

@@ -3,7 +3,6 @@ from typing import Any
from mcp.types import CallToolResult
from astrbot.core.agent.hooks import BaseAgentRunHooks
from astrbot.core.agent.message import Message
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool
from astrbot.core.astr_agent_context import AstrAgentContext
@@ -70,37 +69,6 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
tool_result,
)
# special handle web_search_tavily
platform_name = run_context.context.event.get_platform_name()
if (
platform_name == "webchat"
and tool.name
in [
"web_search_baidu",
"web_search_tavily",
"web_search_bocha",
"web_search_brave",
]
and len(run_context.messages) > 0
and tool_result
and len(tool_result.content)
):
# inject system prompt
first_part = run_context.messages[0]
if (
isinstance(first_part, Message)
and first_part.role == "system"
and first_part.content
and isinstance(first_part.content, str)
):
# we assume system part is str
first_part.content += (
"Always cite web search results you rely on. "
"Index is a unique identifier for each search result. "
"Use the exact citation format <ref>index</ref> (e.g. <ref>abcd.3</ref>) "
"after the sentence that uses the information. Do not invent citations."
)
class EmptyAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
pass

View File

@@ -115,6 +115,20 @@ from astrbot.core.utils.quoted_message_parser import (
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
LLM_ERROR_MESSAGE_EXTRA_KEY = "_llm_error_message"
WEB_SEARCH_CITATION_TOOL_NAMES = frozenset(
{
"web_search_baidu",
"web_search_tavily",
"web_search_bocha",
"web_search_brave",
}
)
WEB_SEARCH_CITATION_PROMPT = (
"Always cite web search results you rely on. "
"Index is a unique identifier for each search result. "
"Use the exact citation format <ref>index</ref> (e.g. <ref>abcd.3</ref>) "
"after the sentence that uses the information. Do not invent citations."
)
@dataclass(slots=True)
@@ -1149,6 +1163,23 @@ async def _apply_web_search_tools(
req.func_tool.add_tool(tool_mgr.get_builtin_tool(BaiduWebSearchTool))
def _apply_web_search_citation_prompt(
event: AstrMessageEvent,
req: ProviderRequest,
) -> None:
if event.get_platform_name() != "webchat" or not req.func_tool:
return
if not any(req.func_tool.get_tool(name) for name in WEB_SEARCH_CITATION_TOOL_NAMES):
return
system_prompt = req.system_prompt or ""
if WEB_SEARCH_CITATION_PROMPT in system_prompt:
return
req.system_prompt = f"{system_prompt}\n{WEB_SEARCH_CITATION_PROMPT}\n"
def _get_compress_provider(
config: MainAgentBuildConfig,
plugin_context: Context,
@@ -1520,6 +1551,8 @@ async def build_main_agent(
if action_type == "live":
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
_apply_web_search_citation_prompt(event, req)
reset_coro = agent_runner.reset(
provider=provider,
request=req,

View File

@@ -5,7 +5,7 @@ import os
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.25.3"
VERSION = "4.25.4"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
PERSONAL_WECHAT_CONFIG_METADATA = {
"weixin_oc_base_url": {
@@ -417,7 +417,7 @@ CONFIG_METADATA_2 = {
"weixin_oc_bot_type": "3",
"weixin_oc_qr_poll_interval": 1,
"weixin_oc_long_poll_timeout_ms": 35_000,
"weixin_oc_api_timeout_ms": 15_000,
"weixin_oc_api_timeout_ms": 120_000,
},
"飞书(Lark)": {
"id": "lark",

View File

@@ -130,7 +130,7 @@ class WeixinOCAdapter(Platform):
platform_config.get("weixin_oc_long_poll_timeout_ms", 35_000),
)
self.api_timeout_ms = int(
platform_config.get("weixin_oc_api_timeout_ms", 15_000),
platform_config.get("weixin_oc_api_timeout_ms", 120_000),
)
self.cdn_base_url = str(
platform_config.get(

View File

@@ -302,12 +302,14 @@ class ProviderAnthropic(Provider):
return system_prompt, new_messages
def _extract_usage(self, usage: Usage) -> TokenUsage:
def _extract_usage(self, usage: Usage | None) -> TokenUsage:
if usage is None:
return TokenUsage()
# https://docs.claude.com/en/docs/build-with-claude/prompt-caching#tracking-cache-performance
return TokenUsage(
input_other=usage.input_tokens or 0,
input_cached=usage.cache_read_input_tokens or 0,
output=usage.output_tokens,
output=usage.output_tokens or 0,
)
def _update_usage(self, token_usage: TokenUsage, usage: MessageDeltaUsage) -> None:

35
changelogs/v4.25.4.md Normal file
View File

@@ -0,0 +1,35 @@
- [更新日志(简体中文)](#chinese)
- [Changelog(English)](#english)
<a id="chinese"></a>
## What's Changed
### 修复
- 回滚部分改动,修复偶现的 `Database is locked` 的问题。([#8639](https://github.com/AstrBotDevs/AstrBot/pull/8639))
- 修复 Pipeline 异步任务可能因缺少强引用而被垃圾回收的问题,提升事件处理稳定性。([#8618](https://github.com/AstrBotDevs/AstrBot/pull/8618))
- 修复 WebChat 使用 Web 搜索工具时,引用提示词在同一轮对话多次工具调用后被重复追加到系统消息的问题,避免破坏上下文缓存。([#8642](https://github.com/AstrBotDevs/AstrBot/pull/8642))
- 同步 Dashboard `pnpm-lock.yaml` 中的 overrides 配置,修复锁文件与工作区配置不一致的问题。([#8637](https://github.com/AstrBotDevs/AstrBot/pull/8637))
### 优化
- 将微信公众号 HTTP API 请求超时时间从 15 秒提升到 120 秒,降低较慢网络或接口响应下下载文件超时失败概率。([#8643](https://github.com/AstrBotDevs/AstrBot/pull/8643))
- Dashboard 登录表单启用完整凭据自动填充,改善浏览器密码管理器的使用体验。([#8631](https://github.com/AstrBotDevs/AstrBot/pull/8631))
<a id="english"></a>
## What's Changed (EN)
### Bug Fixes
- Fixed repeated Web search citation prompt appends in WebChat after multiple tool calls within the same interaction, preventing context cache invalidation. ([#8642](https://github.com/AstrBotDevs/AstrBot/pull/8642))
- Fixed Pipeline async tasks potentially being garbage-collected due to missing strong references, improving event processing stability. ([#8618](https://github.com/AstrBotDevs/AstrBot/pull/8618))
- Synced Dashboard `pnpm-lock.yaml` overrides with the workspace configuration. ([#8637](https://github.com/AstrBotDevs/AstrBot/pull/8637))
- Reverted the Provider stats SQLite lock retry change to avoid related regressions. ([#8639](https://github.com/AstrBotDevs/AstrBot/pull/8639))
- Reverted the macOS SQLAlchemy compatibility changes to avoid regressions in database initialization and vector storage paths. ([#8638](https://github.com/AstrBotDevs/AstrBot/pull/8638))
### Improvements
- Increased the WeChat Official Account HTTP API request timeout from 15 seconds to 120 seconds, reducing timeout failures on slower networks or API responses. ([#8643](https://github.com/AstrBotDevs/AstrBot/pull/8643))
- Enabled full credential autofill on the Dashboard login form for better browser password manager support. ([#8631](https://github.com/AstrBotDevs/AstrBot/pull/8631))

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.25.3"
version = "4.25.4"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
license = { text = "AGPL-3.0-or-later" }

View File

@@ -483,6 +483,40 @@ def _setup_provider_with_mock_client(monkeypatch) -> anthropic_source.ProviderAn
return provider
@pytest.mark.asyncio
async def test_query_handles_none_usage_when_content_filtered(monkeypatch):
provider = _setup_provider_with_mock_client(monkeypatch)
content_filter_message = (
"The request was rejected because it was considered high risk"
)
class _FakeMessageBlock:
def __init__(self, text: str):
self.type = "text"
self.text = text
class _FakeMessage:
def __init__(self):
self.id = "msg_content_filter"
self.content = [_FakeMessageBlock(content_filter_message)]
self.stop_reason = "content_filter"
self.usage = None
async def fake_create(**kwargs):
return _FakeMessage()
monkeypatch.setattr(anthropic_source, "Message", _FakeMessage)
provider.client.messages.create = fake_create
llm_response = await provider.text_chat(prompt="test")
assert llm_response.completion_text == content_filter_message
assert llm_response.usage is not None
assert llm_response.usage.input_other == 0
assert llm_response.usage.input_cached == 0
assert llm_response.usage.output == 0
@pytest.mark.asyncio
async def test_tool_choice_auto_converts_to_dict(monkeypatch):
"""tool_choice='auto' 应转换为 {'type': 'auto'}"""

View File

@@ -476,6 +476,46 @@ class TestBuiltinToolInjection:
assert req.func_tool.get_tool("web_search_firecrawl") is search_tool
assert req.func_tool.get_tool("firecrawl_extract_web_page") is extract_tool
def test_apply_web_search_citation_prompt_for_webchat(self, mock_event):
module = ama
req = ProviderRequest(system_prompt="base")
search_tool = MagicMock(spec=FunctionTool)
search_tool.name = "web_search_tavily"
req.func_tool = ToolSet()
req.func_tool.add_tool(search_tool)
mock_event.get_platform_name.return_value = "webchat"
module._apply_web_search_citation_prompt(mock_event, req)
assert module.WEB_SEARCH_CITATION_PROMPT in req.system_prompt
def test_apply_web_search_citation_prompt_is_idempotent(self, mock_event):
module = ama
req = ProviderRequest(system_prompt="")
search_tool = MagicMock(spec=FunctionTool)
search_tool.name = "web_search_tavily"
req.func_tool = ToolSet()
req.func_tool.add_tool(search_tool)
mock_event.get_platform_name.return_value = "webchat"
module._apply_web_search_citation_prompt(mock_event, req)
module._apply_web_search_citation_prompt(mock_event, req)
assert req.system_prompt.count(module.WEB_SEARCH_CITATION_PROMPT) == 1
def test_apply_web_search_citation_prompt_requires_webchat(self, mock_event):
module = ama
req = ProviderRequest(system_prompt="")
search_tool = MagicMock(spec=FunctionTool)
search_tool.name = "web_search_tavily"
req.func_tool = ToolSet()
req.func_tool.add_tool(search_tool)
mock_event.get_platform_name.return_value = "test_platform"
module._apply_web_search_citation_prompt(mock_event, req)
assert module.WEB_SEARCH_CITATION_PROMPT not in req.system_prompt
def test_proactive_cron_job_tools_uses_builtin_tool_manager(self, mock_context):
"""Test cron tool injection through the builtin tool manager."""
module = ama