Compare commits

...

7 Commits

Author SHA1 Message Date
Soulter d1759ca2ed docs: revise description for AstrBot in README.md 2026-02-05 13:35:49 +08:00
Soulter 912e40e7f0 chore: delete unused file 2026-02-05 10:40:53 +08:00
Xican 2876c43387 fix: 修复特定提供商导致的定时任务执行失败的问题 (#4872)
* fix: 修复特定提供商导致的定时任务执行失败的问题

* ruff format

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-05 10:14:31 +08:00
Soulter 464882f206 chore: bump version to 4.14.4 2026-02-04 23:21:08 +08:00
Soulter 6736fb85c2 fix: conversation token usage calculate wrongly and fix tool call infinitely (#4869) 2026-02-04 23:18:32 +08:00
Soulter 1f75255950 chore: bump version to 4.14.3 2026-02-04 20:31:19 +08:00
Soulter a954e75547 fix: add apply_reset parameter to build_main_agent and handle coroutine reset in InternalAgentSubStage 2026-02-04 20:25:31 +08:00
11 changed files with 35 additions and 25 deletions
-18
View File
@@ -1,18 +0,0 @@
我需要让 Agent 能够在未来提醒自己去做某些事情,这样 Agent 能够主动地去完成一些任务,而不是等用户主动来下达命令。
你需要实现一个 CronJob 系统,允许 Agent 创建未来任务,并且在未来的某个时间点自动触发这些任务的执行.
CronJob 系统分为 BasicCronJob 和 ActiveAgentCronJob 两种类型。前者只是简单的提供一个定时任务功能(给插件用),而后者则允许 Agent 主动地去完成一些任务。BasicCronJob 不必多说,就是定时执行某个函数。对于 ActiveAgentCronJobAgent 应该可以主动管理(比如通过Tool来管理)这些 CronJobs,当添加的时候,Agent 可以给 CronJob 捎一段文字,以说明未来的自己需要做什么事情。比如说,Agent 在听到用户 “每天早上都给我整理一份今日早报” 之后,应该可以创建 Cron Job,并且自己写脚本来完成这个任务,并且注册 cron job。Agent 给未来的自己捎去的信息应该只是呈现为一段文字,这样可以保持设计简约。当触发后, CronJobManager 会调用 MainAgent 的一轮循环,MainAgent 通过上下文知道这是一个定时任务触发的循环,从而执行相应的操作。
此外,我还有一个需求,后台长任务。需要给当前的 FunctionTool 类增加一个属性,is_background_task: bool = False,插件可以通过这个属性来声明这是一个异步任务。这是为了解决一些 Tool 需要长时间运行的问题,比如 Deep Search tool 需要长时间搜索网页内容、Sub Agent 需要长时间运行来完成一个复杂任务。
基于上面的讨论,我觉得,应该:
1. 需要给当前的 FunctionTool 类增加一个属性is_background_task: bool = Falsetool runner 在执行这个 tool 的时候,如果发现是后台任务,就不等待结果返回,而是直接返回一个任务 ID (已经创建成功提示)的结果,tool runner 在后台继续执行这个任务。当任务完成之后,任务的结果回传给 MainAgent(其实就是再执行一次 main agent loop,但是上下文应该是最新的),并且 MainAgent 此时应该有 send_message_to_user 的工具,通过这个工具可以选择是否主动通知用户任务完成的结果。
2. 增加一个 CronJobManager 类,负责管理所有的定时任务。Agent 可以通过调用这个类的方法来创建、删除、修改定时任务。通过 cron expression 来定义触发条件。
3. CronJobManager 除了管理普通的定时任务(比如插件可能有一些自己的定时任务),还有一种特殊的任务类型,就是上面提到的主动型 Agent 任务。用户提需求,MainAgent 选择性地调用 CronJobManager 的方法来创建这些任务,并且在任务触发时,CronJobManager 的回调就是执行 MainAgent 的一轮循环(需要加 send_message_to_user tool),MainAgent 通过上下文知道这是一个定时任务触发的循环,从而执行相应的操作。
4. WebUI 需要增加 Cron Job 管理界面,用户可以在界面上查看、创建、修改、删除定时任务。对于主动型 Agent 任务,用户可以看到任务的描述、触发条件等信息。
5. 除此之外,现在的代码中已经有了 subagent 的管理。WebUI 可以创建 SubAgent,但是还没写完。除了结合上面我说的之外,你还需要将 SubAgent 与 Persona 结合起来——因为 Persona 是一个包含了 tool、skills、name、description 的完整体,所以 SubAgent 应该直接继承 Persona 的定义,而不是单独定义 SubAgent。SubAgent 本质上就是一个有特定角色和能力的 Persona!多么美妙的设计啊!
6. 为了实现大一统,is_background_task = True 的时候,后台任务也挂到 CronJobManager 上去管理,只不过这个是立即触发的任务,不需要等到未来某个时间点才触发罢了。
我希望设计尽可能简单,但是强大。
+1 -1
View File
@@ -34,7 +34,7 @@
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a> <a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
</div> </div>
AstrBot 是一个开源的一站式 Agentic 个人群聊助手可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。 AstrBot 是一个易用、高性能的 AI Agentic 个人 / 群聊助手可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba) ![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.14.2" __version__ = "4.14.4"
@@ -213,6 +213,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
if not llm_response.is_chunk and llm_response.usage: if not llm_response.is_chunk and llm_response.usage:
# only count the token usage of the final response for computation purpose # only count the token usage of the final response for computation purpose
self.stats.token_usage += llm_response.usage self.stats.token_usage += llm_response.usage
if self.req.conversation:
self.req.conversation.token_usage = llm_response.usage.total
break # got final response break # got final response
if not llm_resp_result: if not llm_resp_result:
+12 -2
View File
@@ -7,6 +7,7 @@ import datetime
import json import json
import os import os
import zoneinfo import zoneinfo
from collections.abc import Coroutine
from dataclasses import dataclass, field from dataclasses import dataclass, field
from astrbot.api import sp from astrbot.api import sp
@@ -114,6 +115,7 @@ class MainAgentBuildResult:
agent_runner: AgentRunner agent_runner: AgentRunner
provider_request: ProviderRequest provider_request: ProviderRequest
provider: Provider provider: Provider
reset_coro: Coroutine | None = None
def _select_provider( def _select_provider(
@@ -837,8 +839,12 @@ async def build_main_agent(
config: MainAgentBuildConfig, config: MainAgentBuildConfig,
provider: Provider | None = None, provider: Provider | None = None,
req: ProviderRequest | None = None, req: ProviderRequest | None = None,
apply_reset: bool = True,
) -> MainAgentBuildResult | None: ) -> MainAgentBuildResult | None:
"""构建主对话代理(Main Agent),并且自动 reset。""" """构建主对话代理(Main Agent),并且自动 reset。
If apply_reset is False, will not call reset on the agent runner.
"""
provider = provider or _select_provider(event, plugin_context) provider = provider or _select_provider(event, plugin_context)
if provider is None: if provider is None:
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。") logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
@@ -955,7 +961,7 @@ async def build_main_agent(
if action_type == "live": if action_type == "live":
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n" req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
await agent_runner.reset( reset_coro = agent_runner.reset(
provider=provider, provider=provider,
request=req, request=req,
run_context=AgentContextWrapper( run_context=AgentContextWrapper(
@@ -973,8 +979,12 @@ async def build_main_agent(
tool_schema_mode=config.tool_schema_mode, tool_schema_mode=config.tool_schema_mode,
) )
if apply_reset:
await reset_coro
return MainAgentBuildResult( return MainAgentBuildResult(
agent_runner=agent_runner, agent_runner=agent_runner,
provider_request=req, provider_request=req,
provider=provider, provider=provider,
reset_coro=reset_coro if not apply_reset else None,
) )
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.14.2" VERSION = "4.14.4"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [ WEBHOOK_SUPPORTED_PLATFORMS = [
+1
View File
@@ -310,6 +310,7 @@ class CronJobManager:
config = MainAgentBuildConfig( config = MainAgentBuildConfig(
tool_call_timeout=3600, tool_call_timeout=3600,
llm_safety_mode=False, llm_safety_mode=False,
streaming_response=False,
) )
req = ProviderRequest() req = ProviderRequest()
conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx) conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx)
@@ -164,6 +164,7 @@ class InternalAgentSubStage(Stage):
event=event, event=event,
plugin_context=self.ctx.plugin_manager.context, plugin_context=self.ctx.plugin_manager.context,
config=build_cfg, config=build_cfg,
apply_reset=False,
) )
if build_result is None: if build_result is None:
@@ -172,6 +173,7 @@ class InternalAgentSubStage(Stage):
agent_runner = build_result.agent_runner agent_runner = build_result.agent_runner
req = build_result.provider_request req = build_result.provider_request
provider = build_result.provider provider = build_result.provider
reset_coro = build_result.reset_coro
api_base = provider.provider_config.get("api_base", "") api_base = provider.provider_config.get("api_base", "")
for host in decoded_blocked: for host in decoded_blocked:
@@ -190,6 +192,10 @@ class InternalAgentSubStage(Stage):
if await call_event_hook(event, EventType.OnLLMRequestEvent, req): if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
return return
# apply reset
if reset_coro:
await reset_coro
action_type = event.get_extra("action_type") action_type = event.get_extra("action_type")
event.trace.record( event.trace.record(
@@ -357,7 +363,8 @@ class InternalAgentSubStage(Stage):
token_usage = None token_usage = None
if runner_stats: if runner_stats:
token_usage = runner_stats.token_usage.total # token_usage = runner_stats.token_usage.total
token_usage = llm_response.usage.total if llm_response.usage else None
await self.conv_manager.update_conversation( await self.conv_manager.update_conversation(
event.unified_msg_origin, event.unified_msg_origin,
+4
View File
@@ -0,0 +1,4 @@
## What's Changed
### 修复
- 修复 `on_llm_request` 钩子可能无法应用效果的问题
+4
View File
@@ -0,0 +1,4 @@
## What's Changed
### 修复
- 修复 token 统计错误的问题,修复在多轮 tool call 情况下或者其他极端情况下可能造成 tool 无限调用的问题。
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "AstrBot" name = "AstrBot"
version = "4.14.2" version = "4.14.4"
description = "Easy-to-use multi-platform LLM chatbot and development framework" description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"