AstrBot 4.13.2__py3-none-any.whl → 4.14.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/builtin_stars/astrbot/main.py +0 -6
- astrbot/builtin_stars/session_controller/main.py +1 -2
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/agent.py +2 -1
- astrbot/core/agent/handoff.py +14 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +14 -1
- astrbot/core/agent/tool.py +5 -0
- astrbot/core/astr_agent_run_util.py +21 -3
- astrbot/core/astr_agent_tool_exec.py +178 -3
- astrbot/core/astr_main_agent.py +980 -0
- astrbot/core/astr_main_agent_resources.py +453 -0
- astrbot/core/computer/computer_client.py +10 -1
- astrbot/core/computer/tools/fs.py +22 -14
- astrbot/core/config/default.py +84 -58
- astrbot/core/core_lifecycle.py +43 -1
- astrbot/core/cron/__init__.py +3 -0
- astrbot/core/cron/events.py +67 -0
- astrbot/core/cron/manager.py +376 -0
- astrbot/core/db/__init__.py +60 -0
- astrbot/core/db/po.py +31 -0
- astrbot/core/db/sqlite.py +120 -0
- astrbot/core/event_bus.py +0 -1
- astrbot/core/message/message_event_result.py +21 -3
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +111 -580
- astrbot/core/pipeline/scheduler.py +0 -2
- astrbot/core/platform/astr_message_event.py +5 -5
- astrbot/core/platform/platform.py +9 -0
- astrbot/core/platform/platform_metadata.py +2 -0
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -0
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +1 -0
- astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +1 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +1 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +1 -0
- astrbot/core/provider/entities.py +1 -1
- astrbot/core/skills/skill_manager.py +9 -8
- astrbot/core/star/context.py +8 -0
- astrbot/core/star/filter/custom_filter.py +3 -3
- astrbot/core/star/register/star_handler.py +1 -1
- astrbot/core/subagent_orchestrator.py +96 -0
- astrbot/core/tools/cron_tools.py +174 -0
- astrbot/core/utils/history_saver.py +31 -0
- astrbot/core/utils/trace.py +4 -0
- astrbot/dashboard/routes/__init__.py +4 -0
- astrbot/dashboard/routes/cron.py +174 -0
- astrbot/dashboard/routes/log.py +36 -0
- astrbot/dashboard/routes/plugin.py +11 -0
- astrbot/dashboard/routes/skills.py +12 -37
- astrbot/dashboard/routes/subagent.py +117 -0
- astrbot/dashboard/routes/tools.py +41 -14
- astrbot/dashboard/server.py +3 -0
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/METADATA +21 -2
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/RECORD +57 -51
- astrbot/builtin_stars/astrbot/process_llm_request.py +0 -308
- astrbot/builtin_stars/reminder/main.py +0 -266
- astrbot/builtin_stars/reminder/metadata.yaml +0 -4
- astrbot/core/pipeline/process_stage/utils.py +0 -219
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/WHEEL +0 -0
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -22,6 +22,7 @@ from astrbot.core.message.components import (
|
|
|
22
22
|
from astrbot.core.message.message_event_result import MessageChain, MessageEventResult
|
|
23
23
|
from astrbot.core.platform.message_type import MessageType
|
|
24
24
|
from astrbot.core.provider.entities import ProviderRequest
|
|
25
|
+
from astrbot.core.agent.tool import ToolSet
|
|
25
26
|
from astrbot.core.utils.metrics import Metric
|
|
26
27
|
from astrbot.core.utils.trace import TraceSpan
|
|
27
28
|
|
|
@@ -73,9 +74,6 @@ class AstrMessageEvent(abc.ABC):
|
|
|
73
74
|
self.span = self.trace
|
|
74
75
|
"""事件级 TraceSpan(别名: span)"""
|
|
75
76
|
|
|
76
|
-
self.trace.record("umo", umo=self.unified_msg_origin)
|
|
77
|
-
self.trace.record("event_created", created_at=self.created_at)
|
|
78
|
-
|
|
79
77
|
self._has_send_oper = False
|
|
80
78
|
"""在此次事件中是否有过至少一次发送消息的操作"""
|
|
81
79
|
self.call_llm = False
|
|
@@ -358,6 +356,7 @@ class AstrMessageEvent(abc.ABC):
|
|
|
358
356
|
self,
|
|
359
357
|
prompt: str,
|
|
360
358
|
func_tool_manager=None,
|
|
359
|
+
tool_set: ToolSet | None = None,
|
|
361
360
|
session_id: str = "",
|
|
362
361
|
image_urls: list[str] | None = None,
|
|
363
362
|
contexts: list | None = None,
|
|
@@ -380,7 +379,7 @@ class AstrMessageEvent(abc.ABC):
|
|
|
380
379
|
|
|
381
380
|
contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。
|
|
382
381
|
|
|
383
|
-
func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager()
|
|
382
|
+
func_tool_manager: [Deprecated] 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。已过时,请使用 tool_set 参数代替。
|
|
384
383
|
|
|
385
384
|
conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
|
|
386
385
|
|
|
@@ -396,7 +395,8 @@ class AstrMessageEvent(abc.ABC):
|
|
|
396
395
|
prompt=prompt,
|
|
397
396
|
session_id=session_id,
|
|
398
397
|
image_urls=image_urls,
|
|
399
|
-
func_tool=func_tool_manager,
|
|
398
|
+
# func_tool=func_tool_manager,
|
|
399
|
+
func_tool=tool_set,
|
|
400
400
|
contexts=contexts,
|
|
401
401
|
system_prompt=system_prompt,
|
|
402
402
|
conversation=conversation,
|
|
@@ -90,6 +90,14 @@ class Platform(abc.ABC):
|
|
|
90
90
|
def get_stats(self) -> dict:
|
|
91
91
|
"""获取平台统计信息"""
|
|
92
92
|
meta = self.meta()
|
|
93
|
+
meta_info = {
|
|
94
|
+
"id": meta.id,
|
|
95
|
+
"name": meta.name,
|
|
96
|
+
"display_name": meta.adapter_display_name or meta.name,
|
|
97
|
+
"description": meta.description,
|
|
98
|
+
"support_streaming_message": meta.support_streaming_message,
|
|
99
|
+
"support_proactive_message": meta.support_proactive_message,
|
|
100
|
+
}
|
|
93
101
|
return {
|
|
94
102
|
"id": meta.id or self.config.get("id"),
|
|
95
103
|
"type": meta.name,
|
|
@@ -105,6 +113,7 @@ class Platform(abc.ABC):
|
|
|
105
113
|
if self.last_error
|
|
106
114
|
else None,
|
|
107
115
|
"unified_webhook": self.unified_webhook(),
|
|
116
|
+
"meta": meta_info,
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
@abc.abstractmethod
|
|
@@ -62,6 +62,7 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
|
|
|
62
62
|
# Based on openai/codex
|
|
63
63
|
return (
|
|
64
64
|
"## Skills\n"
|
|
65
|
+
"You have many useful skills that can help you accomplish various tasks.\n"
|
|
65
66
|
"A skill is a set of local instructions stored in a `SKILL.md` file.\n"
|
|
66
67
|
"### Available skills\n"
|
|
67
68
|
f"{skills_block}\n"
|
|
@@ -69,21 +70,21 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
|
|
|
69
70
|
"\n"
|
|
70
71
|
"- Discovery: The list above shows all skills available in this session. Full instructions live in the referenced `SKILL.md`.\n"
|
|
71
72
|
"- Trigger rules: Use a skill if the user names it or the task matches its description. Do not carry skills across turns unless re-mentioned\n"
|
|
72
|
-
"- Unavailable: If a skill is missing or unreadable, say so and fallback.\n"
|
|
73
73
|
"### How to use a skill (progressive disclosure):\n"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
74
|
+
" 0) Mandatory grounding: Before using any skill, you MUST inspect its `SKILL.md` using shell tools"
|
|
75
|
+
" (e.g., `cat`, `head`, `sed`, `awk`, `grep`). Do not rely on assumptions or memory.\n"
|
|
76
|
+
" 1) Load only directly referenced files, DO NOT bulk-load everything.\n"
|
|
77
|
+
" 2) If `scripts/` exist, prefer running or patching them instead of retyping large blocks of code.\n"
|
|
78
|
+
" 3) If `assets/` or templates exist, reuse them rather than recreating everything from scratch.\n"
|
|
78
79
|
"- Coordination:\n"
|
|
79
80
|
" - If multiple skills apply, choose the minimal set that covers the request and state the order in which you will use them.\n"
|
|
80
81
|
" - Announce which skill(s) you are using and why (one short line). If you skip an obvious skill, explain why.\n"
|
|
81
82
|
" - Prefer to use `astrbot_*` tools to perform skills that need to run scripts.\n"
|
|
82
83
|
"- Context hygiene:\n"
|
|
83
|
-
" - Keep context small: summarize long sections instead of pasting them, and load extra files only when necessary.\n"
|
|
84
84
|
" - Avoid deep reference chasing: unless blocked, open only files that are directly linked from `SKILL.md`.\n"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
85
|
+
"- Failure handling: If a skill cannot be applied, state the issue and continue with the best alternative.\n"
|
|
86
|
+
"### Example\n"
|
|
87
|
+
"When you decided to use a skill, use shell tool to read its `SKILL.md`, e.g., `head -40 skills/code_formatter/SKILL.md`, and you can increase or decrease the number of lines as needed.\n"
|
|
87
88
|
)
|
|
88
89
|
|
|
89
90
|
|
astrbot/core/star/context.py
CHANGED
|
@@ -12,6 +12,7 @@ from astrbot.core.agent.tool import ToolSet
|
|
|
12
12
|
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
|
13
13
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
|
14
14
|
from astrbot.core.conversation_mgr import ConversationManager
|
|
15
|
+
from astrbot.core.cron.manager import CronJobManager
|
|
15
16
|
from astrbot.core.db import BaseDatabase
|
|
16
17
|
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
|
17
18
|
from astrbot.core.message.message_event_result import MessageChain
|
|
@@ -34,6 +35,7 @@ from astrbot.core.star.filter.platform_adapter_type import (
|
|
|
34
35
|
ADAPTER_NAME_2_TYPE,
|
|
35
36
|
PlatformAdapterType,
|
|
36
37
|
)
|
|
38
|
+
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
|
|
37
39
|
|
|
38
40
|
from ..exceptions import ProviderNotFoundError
|
|
39
41
|
from .filter.command import CommandFilter
|
|
@@ -65,6 +67,8 @@ class Context:
|
|
|
65
67
|
persona_manager: PersonaManager,
|
|
66
68
|
astrbot_config_mgr: AstrBotConfigManager,
|
|
67
69
|
knowledge_base_manager: KnowledgeBaseManager,
|
|
70
|
+
cron_manager: CronJobManager,
|
|
71
|
+
subagent_orchestrator: SubAgentOrchestrator | None = None,
|
|
68
72
|
):
|
|
69
73
|
self._event_queue = event_queue
|
|
70
74
|
"""事件队列。消息平台通过事件队列传递消息事件。"""
|
|
@@ -86,6 +90,9 @@ class Context:
|
|
|
86
90
|
"""配置文件管理器(非webui)"""
|
|
87
91
|
self.kb_manager = knowledge_base_manager
|
|
88
92
|
"""知识库管理器"""
|
|
93
|
+
self.cron_manager = cron_manager
|
|
94
|
+
"""Cron job manager, initialized by core lifecycle."""
|
|
95
|
+
self.subagent_orchestrator = subagent_orchestrator
|
|
89
96
|
|
|
90
97
|
async def llm_generate(
|
|
91
98
|
self,
|
|
@@ -463,6 +470,7 @@ class Context:
|
|
|
463
470
|
_parts.append(part)
|
|
464
471
|
if part in flags and i + 1 < len(module_part):
|
|
465
472
|
_parts.append(module_part[i + 1])
|
|
473
|
+
module_part.append("main")
|
|
466
474
|
break
|
|
467
475
|
tool.handler_module_path = ".".join(_parts)
|
|
468
476
|
module_path = tool.handler_module_path
|
|
@@ -37,9 +37,9 @@ class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
|
|
|
37
37
|
class CustomFilterOr(CustomFilter):
|
|
38
38
|
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
|
39
39
|
super().__init__()
|
|
40
|
-
if not isinstance(filter1, CustomFilter
|
|
40
|
+
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
|
41
41
|
raise ValueError(
|
|
42
|
-
"CustomFilter
|
|
42
|
+
"CustomFilter class can only operate with other CustomFilter.",
|
|
43
43
|
)
|
|
44
44
|
self.filter1 = filter1
|
|
45
45
|
self.filter2 = filter2
|
|
@@ -51,7 +51,7 @@ class CustomFilterOr(CustomFilter):
|
|
|
51
51
|
class CustomFilterAnd(CustomFilter):
|
|
52
52
|
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
|
53
53
|
super().__init__()
|
|
54
|
-
if not isinstance(filter1, CustomFilter
|
|
54
|
+
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
|
55
55
|
raise ValueError(
|
|
56
56
|
"CustomFilter lass can only operate with other CustomFilter.",
|
|
57
57
|
)
|
|
@@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs):
|
|
|
150
150
|
if args:
|
|
151
151
|
raise_error = args[0]
|
|
152
152
|
|
|
153
|
-
if not isinstance(custom_filter, CustomFilterAnd
|
|
153
|
+
if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
|
|
154
154
|
custom_filter = custom_filter(raise_error)
|
|
155
155
|
|
|
156
156
|
def decorator(awaitable):
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from astrbot import logger
|
|
6
|
+
from astrbot.core.agent.agent import Agent
|
|
7
|
+
from astrbot.core.agent.handoff import HandoffTool
|
|
8
|
+
from astrbot.core.persona_mgr import PersonaManager
|
|
9
|
+
from astrbot.core.provider.func_tool_manager import FunctionToolManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SubAgentOrchestrator:
|
|
13
|
+
"""Loads subagent definitions from config and registers handoff tools.
|
|
14
|
+
|
|
15
|
+
This is intentionally lightweight: it does not execute agents itself.
|
|
16
|
+
Execution happens via HandoffTool in FunctionToolExecutor.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager):
|
|
20
|
+
self._tool_mgr = tool_mgr
|
|
21
|
+
self._persona_mgr = persona_mgr
|
|
22
|
+
self.handoffs: list[HandoffTool] = []
|
|
23
|
+
|
|
24
|
+
async def reload_from_config(self, cfg: dict[str, Any]) -> None:
|
|
25
|
+
from astrbot.core.astr_agent_context import AstrAgentContext
|
|
26
|
+
|
|
27
|
+
agents = cfg.get("agents", [])
|
|
28
|
+
if not isinstance(agents, list):
|
|
29
|
+
logger.warning("subagent_orchestrator.agents must be a list")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
handoffs: list[HandoffTool] = []
|
|
33
|
+
for item in agents:
|
|
34
|
+
if not isinstance(item, dict):
|
|
35
|
+
continue
|
|
36
|
+
if not item.get("enabled", True):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
name = str(item.get("name", "")).strip()
|
|
40
|
+
if not name:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
persona_id = item.get("persona_id")
|
|
44
|
+
persona_data = None
|
|
45
|
+
if persona_id:
|
|
46
|
+
try:
|
|
47
|
+
persona_data = await self._persona_mgr.get_persona(persona_id)
|
|
48
|
+
except StopIteration:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"SubAgent persona %s not found, fallback to inline prompt.",
|
|
51
|
+
persona_id,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
instructions = str(item.get("system_prompt", "")).strip()
|
|
55
|
+
public_description = str(item.get("public_description", "")).strip()
|
|
56
|
+
provider_id = item.get("provider_id")
|
|
57
|
+
if provider_id is not None:
|
|
58
|
+
provider_id = str(provider_id).strip() or None
|
|
59
|
+
tools = item.get("tools", [])
|
|
60
|
+
begin_dialogs = None
|
|
61
|
+
|
|
62
|
+
if persona_data:
|
|
63
|
+
instructions = persona_data.system_prompt or instructions
|
|
64
|
+
begin_dialogs = persona_data.begin_dialogs
|
|
65
|
+
tools = persona_data.tools
|
|
66
|
+
if public_description == "" and persona_data.system_prompt:
|
|
67
|
+
public_description = persona_data.system_prompt[:120]
|
|
68
|
+
if tools is None:
|
|
69
|
+
tools = None
|
|
70
|
+
elif not isinstance(tools, list):
|
|
71
|
+
tools = []
|
|
72
|
+
else:
|
|
73
|
+
tools = [str(t).strip() for t in tools if str(t).strip()]
|
|
74
|
+
|
|
75
|
+
agent = Agent[AstrAgentContext](
|
|
76
|
+
name=name,
|
|
77
|
+
instructions=instructions,
|
|
78
|
+
tools=tools, # type: ignore
|
|
79
|
+
)
|
|
80
|
+
agent.begin_dialogs = begin_dialogs
|
|
81
|
+
# The tool description should be a short description for the main LLM,
|
|
82
|
+
# while the subagent system prompt can be longer/more specific.
|
|
83
|
+
handoff = HandoffTool(
|
|
84
|
+
agent=agent,
|
|
85
|
+
tool_description=public_description or None,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Optional per-subagent chat provider override.
|
|
89
|
+
handoff.provider_id = provider_id
|
|
90
|
+
|
|
91
|
+
handoffs.append(handoff)
|
|
92
|
+
|
|
93
|
+
for handoff in handoffs:
|
|
94
|
+
logger.info(f"Registered subagent handoff tool: {handoff.name}")
|
|
95
|
+
|
|
96
|
+
self.handoffs = handoffs
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic.dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from astrbot.core.agent.run_context import ContextWrapper
|
|
7
|
+
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
|
8
|
+
from astrbot.core.astr_agent_context import AstrAgentContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
|
|
13
|
+
name: str = "create_future_task"
|
|
14
|
+
description: str = (
|
|
15
|
+
"Create a future task for your future. Supports recurring cron expressions or one-time run_at datetime. "
|
|
16
|
+
"Use this when you or the user want scheduled follow-up or proactive actions."
|
|
17
|
+
)
|
|
18
|
+
parameters: dict = Field(
|
|
19
|
+
default_factory=lambda: {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"cron_expression": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Cron expression defining recurring schedule (e.g., '0 8 * * *').",
|
|
25
|
+
},
|
|
26
|
+
"run_at": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "ISO datetime for one-time execution, e.g., 2026-02-02T08:00:00+08:00. Use with run_once=true.",
|
|
29
|
+
},
|
|
30
|
+
"note": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Detailed instructions for your future agent to execute when it wakes.",
|
|
33
|
+
},
|
|
34
|
+
"name": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Optional label to recognize this future task.",
|
|
37
|
+
},
|
|
38
|
+
"run_once": {
|
|
39
|
+
"type": "boolean",
|
|
40
|
+
"description": "If true, the task will run only once and then be deleted. Use run_at to specify the time.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
"required": ["note"],
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def call(
|
|
48
|
+
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
|
49
|
+
) -> ToolExecResult:
|
|
50
|
+
cron_mgr = context.context.context.cron_manager
|
|
51
|
+
if cron_mgr is None:
|
|
52
|
+
return "error: cron manager is not available."
|
|
53
|
+
|
|
54
|
+
cron_expression = kwargs.get("cron_expression")
|
|
55
|
+
run_at = kwargs.get("run_at")
|
|
56
|
+
run_once = bool(kwargs.get("run_once", False))
|
|
57
|
+
note = str(kwargs.get("note", "")).strip()
|
|
58
|
+
name = str(kwargs.get("name") or "").strip() or "active_agent_task"
|
|
59
|
+
|
|
60
|
+
if not note:
|
|
61
|
+
return "error: note is required."
|
|
62
|
+
if run_once and not run_at:
|
|
63
|
+
return "error: run_at is required when run_once=true."
|
|
64
|
+
if (not run_once) and not cron_expression:
|
|
65
|
+
return "error: cron_expression is required when run_once=false."
|
|
66
|
+
if run_once and cron_expression:
|
|
67
|
+
cron_expression = None
|
|
68
|
+
run_at_dt = None
|
|
69
|
+
if run_at:
|
|
70
|
+
try:
|
|
71
|
+
run_at_dt = datetime.fromisoformat(str(run_at))
|
|
72
|
+
except Exception:
|
|
73
|
+
return "error: run_at must be ISO datetime, e.g., 2026-02-02T08:00:00+08:00"
|
|
74
|
+
|
|
75
|
+
payload = {
|
|
76
|
+
"session": context.context.event.unified_msg_origin,
|
|
77
|
+
"sender_id": context.context.event.get_sender_id(),
|
|
78
|
+
"note": note,
|
|
79
|
+
"origin": "tool",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
job = await cron_mgr.add_active_job(
|
|
83
|
+
name=name,
|
|
84
|
+
cron_expression=str(cron_expression) if cron_expression else None,
|
|
85
|
+
payload=payload,
|
|
86
|
+
description=note,
|
|
87
|
+
run_once=run_once,
|
|
88
|
+
run_at=run_at_dt,
|
|
89
|
+
)
|
|
90
|
+
next_run = job.next_run_time or run_at_dt
|
|
91
|
+
suffix = (
|
|
92
|
+
f"one-time at {next_run}"
|
|
93
|
+
if run_once
|
|
94
|
+
else f"expression '{cron_expression}' (next {next_run})"
|
|
95
|
+
)
|
|
96
|
+
return f"Scheduled future task {job.job_id} ({job.name}) {suffix}."
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class DeleteCronJobTool(FunctionTool[AstrAgentContext]):
|
|
101
|
+
name: str = "delete_future_task"
|
|
102
|
+
description: str = "Delete a future task (cron job) by its job_id."
|
|
103
|
+
parameters: dict = Field(
|
|
104
|
+
default_factory=lambda: {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"job_id": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "The job_id returned when the job was created.",
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"required": ["job_id"],
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async def call(
|
|
117
|
+
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
|
118
|
+
) -> ToolExecResult:
|
|
119
|
+
cron_mgr = context.context.context.cron_manager
|
|
120
|
+
if cron_mgr is None:
|
|
121
|
+
return "error: cron manager is not available."
|
|
122
|
+
job_id = kwargs.get("job_id")
|
|
123
|
+
if not job_id:
|
|
124
|
+
return "error: job_id is required."
|
|
125
|
+
await cron_mgr.delete_job(str(job_id))
|
|
126
|
+
return f"Deleted cron job {job_id}."
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class ListCronJobsTool(FunctionTool[AstrAgentContext]):
|
|
131
|
+
name: str = "list_future_tasks"
|
|
132
|
+
description: str = "List existing future tasks (cron jobs) for inspection."
|
|
133
|
+
parameters: dict = Field(
|
|
134
|
+
default_factory=lambda: {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"job_type": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Optional filter: basic or active_agent.",
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
async def call(
|
|
146
|
+
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
|
147
|
+
) -> ToolExecResult:
|
|
148
|
+
cron_mgr = context.context.context.cron_manager
|
|
149
|
+
if cron_mgr is None:
|
|
150
|
+
return "error: cron manager is not available."
|
|
151
|
+
job_type = kwargs.get("job_type")
|
|
152
|
+
jobs = await cron_mgr.list_jobs(job_type)
|
|
153
|
+
if not jobs:
|
|
154
|
+
return "No cron jobs found."
|
|
155
|
+
lines = []
|
|
156
|
+
for j in jobs:
|
|
157
|
+
lines.append(
|
|
158
|
+
f"{j.job_id} | {j.name} | {j.job_type} | run_once={getattr(j, 'run_once', False)} | enabled={j.enabled} | next={j.next_run_time}"
|
|
159
|
+
)
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
CREATE_CRON_JOB_TOOL = CreateActiveCronTool()
|
|
164
|
+
DELETE_CRON_JOB_TOOL = DeleteCronJobTool()
|
|
165
|
+
LIST_CRON_JOBS_TOOL = ListCronJobsTool()
|
|
166
|
+
|
|
167
|
+
__all__ = [
|
|
168
|
+
"CREATE_CRON_JOB_TOOL",
|
|
169
|
+
"DELETE_CRON_JOB_TOOL",
|
|
170
|
+
"LIST_CRON_JOBS_TOOL",
|
|
171
|
+
"CreateActiveCronTool",
|
|
172
|
+
"DeleteCronJobTool",
|
|
173
|
+
"ListCronJobsTool",
|
|
174
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from astrbot import logger
|
|
4
|
+
from astrbot.core.conversation_mgr import ConversationManager
|
|
5
|
+
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
|
6
|
+
from astrbot.core.provider.entities import ProviderRequest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def persist_agent_history(
|
|
10
|
+
conversation_manager: ConversationManager,
|
|
11
|
+
*,
|
|
12
|
+
event: AstrMessageEvent,
|
|
13
|
+
req: ProviderRequest,
|
|
14
|
+
summary_note: str,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Persist agent interaction into conversation history."""
|
|
17
|
+
if not req or not req.conversation:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
history = []
|
|
21
|
+
try:
|
|
22
|
+
history = json.loads(req.conversation.history or "[]")
|
|
23
|
+
except Exception as exc: # noqa: BLE001
|
|
24
|
+
logger.warning("Failed to parse conversation history: %s", exc)
|
|
25
|
+
history.append({"role": "user", "content": "Output your last task result below."})
|
|
26
|
+
history.append({"role": "assistant", "content": summary_note})
|
|
27
|
+
await conversation_manager.update_conversation(
|
|
28
|
+
event.unified_msg_origin,
|
|
29
|
+
req.conversation.cid,
|
|
30
|
+
history=history,
|
|
31
|
+
)
|
astrbot/core/utils/trace.py
CHANGED
|
@@ -50,6 +50,10 @@ class TraceSpan:
|
|
|
50
50
|
self.started_at = time.time()
|
|
51
51
|
|
|
52
52
|
def record(self, action: str, **fields: Any) -> None:
|
|
53
|
+
# Check if trace recording is enabled
|
|
54
|
+
if not astrbot_config.get("trace_enable", True):
|
|
55
|
+
return
|
|
56
|
+
|
|
53
57
|
payload = {
|
|
54
58
|
"type": "trace",
|
|
55
59
|
"level": "TRACE",
|
|
@@ -5,6 +5,7 @@ from .chatui_project import ChatUIProjectRoute
|
|
|
5
5
|
from .command import CommandRoute
|
|
6
6
|
from .config import ConfigRoute
|
|
7
7
|
from .conversation import ConversationRoute
|
|
8
|
+
from .cron import CronRoute
|
|
8
9
|
from .file import FileRoute
|
|
9
10
|
from .knowledge_base import KnowledgeBaseRoute
|
|
10
11
|
from .log import LogRoute
|
|
@@ -15,6 +16,7 @@ from .session_management import SessionManagementRoute
|
|
|
15
16
|
from .skills import SkillsRoute
|
|
16
17
|
from .stat import StatRoute
|
|
17
18
|
from .static_file import StaticFileRoute
|
|
19
|
+
from .subagent import SubAgentRoute
|
|
18
20
|
from .tools import ToolsRoute
|
|
19
21
|
from .update import UpdateRoute
|
|
20
22
|
|
|
@@ -26,6 +28,7 @@ __all__ = [
|
|
|
26
28
|
"CommandRoute",
|
|
27
29
|
"ConfigRoute",
|
|
28
30
|
"ConversationRoute",
|
|
31
|
+
"CronRoute",
|
|
29
32
|
"FileRoute",
|
|
30
33
|
"KnowledgeBaseRoute",
|
|
31
34
|
"LogRoute",
|
|
@@ -35,6 +38,7 @@ __all__ = [
|
|
|
35
38
|
"SessionManagementRoute",
|
|
36
39
|
"StatRoute",
|
|
37
40
|
"StaticFileRoute",
|
|
41
|
+
"SubAgentRoute",
|
|
38
42
|
"ToolsRoute",
|
|
39
43
|
"SkillsRoute",
|
|
40
44
|
"UpdateRoute",
|