diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py
index b3ea355b1..56066c561 100644
--- a/astrbot/builtin_stars/astrbot/main.py
+++ b/astrbot/builtin_stars/astrbot/main.py
@@ -7,7 +7,6 @@ from astrbot.api.provider import LLMResponse, ProviderRequest
from astrbot.core import logger
from .long_term_memory import LongTermMemory
-from .process_llm_request import ProcessLLMRequest
class Main(star.Star):
@@ -19,8 +18,6 @@ class Main(star.Star):
except BaseException as e:
logger.error(f"聊天增强 err: {e}")
- self.proc_llm_req = ProcessLLMRequest(self.context)
-
def ltm_enabled(self, event: AstrMessageEvent):
ltmse = self.context.get_config(umo=event.unified_msg_origin)[
"provider_ltm_settings"
@@ -91,8 +88,6 @@ class Main(star.Star):
@filter.on_llm_request()
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
- await self.proc_llm_req.process_llm_request(event, req)
-
if self.ltm and self.ltm_enabled(event):
try:
await self.ltm.on_req_llm(event, req)
diff --git a/astrbot/builtin_stars/astrbot/process_llm_request.py b/astrbot/builtin_stars/astrbot/process_llm_request.py
deleted file mode 100644
index 2ecfeac49..000000000
--- a/astrbot/builtin_stars/astrbot/process_llm_request.py
+++ /dev/null
@@ -1,401 +0,0 @@
-import builtins
-import copy
-import datetime
-import zoneinfo
-
-from astrbot.api import logger, sp, star
-from astrbot.api.event import AstrMessageEvent
-from astrbot.api.message_components import Image, Reply
-from astrbot.api.provider import Provider, ProviderRequest
-from astrbot.core.agent.handoff import HandoffTool
-from astrbot.core.agent.message import TextPart
-from astrbot.core.astr_main_agent_resources import (
- CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
- LOCAL_EXECUTE_SHELL_TOOL,
- LOCAL_PYTHON_TOOL,
-)
-from astrbot.core.provider.func_tool_manager import ToolSet
-from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
-
-
-class ProcessLLMRequest:
- def __init__(self, context: star.Context):
- self.ctx = context
- cfg = context.get_config()
- self.timezone = cfg.get("timezone")
- if not self.timezone:
- # 系统默认时区
- self.timezone = None
- else:
- logger.info(f"Timezone set to: {self.timezone}")
-
- self.skill_manager = SkillManager()
-
- def _apply_local_env_tools(self, req: ProviderRequest) -> None:
- """Add local environment tools to the provider request."""
- if req.func_tool is None:
- req.func_tool = ToolSet()
- req.func_tool.add_tool(LOCAL_EXECUTE_SHELL_TOOL)
- req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
-
- async def _ensure_persona(
- self,
- req: ProviderRequest,
- cfg: dict,
- umo: str,
- platform_type: str,
- event: AstrMessageEvent,
- ):
- """确保用户人格已加载"""
- if not req.conversation:
- return
- # persona inject
-
- # custom rule is preferred
- persona_id = (
- await sp.get_async(
- scope="umo", scope_id=umo, key="session_service_config", default={}
- )
- ).get("persona_id")
-
- if not persona_id:
- persona_id = req.conversation.persona_id or cfg.get("default_personality")
- if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
- default_persona = self.ctx.persona_manager.selected_default_persona_v3
- if default_persona:
- persona_id = default_persona["name"]
-
- # ChatUI special default persona
- if platform_type == "webchat":
- # non-existent persona_id to let following codes not working
- persona_id = "_chatui_default_"
- req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
-
- persona = next(
- builtins.filter(
- lambda persona: persona["name"] == persona_id,
- self.ctx.persona_manager.personas_v3,
- ),
- None,
- )
- if persona:
- if prompt := persona["prompt"]:
- req.system_prompt += prompt
- if begin_dialogs := copy.deepcopy(persona["_begin_dialogs_processed"]):
- req.contexts[:0] = begin_dialogs
-
- # skills select and prompt
- runtime = self.skills_cfg.get("runtime", "local")
- skills = self.skill_manager.list_skills(active_only=True, runtime=runtime)
- if runtime == "sandbox" and not self.sandbox_cfg.get("enable", False):
- logger.warning(
- "Skills runtime is set to sandbox, but sandbox mode is disabled, will skip skills prompt injection.",
- )
- req.system_prompt += "\n[Background: User added some skills, and skills runtime is set to sandbox, but sandbox mode is disabled. So skills will be unavailable.]\n"
- elif skills:
- # persona.skills == None means all skills are allowed
- if persona and persona.get("skills") is not None:
- if not persona["skills"]:
- return
- allowed = set(persona["skills"])
- skills = [skill for skill in skills if skill.name in allowed]
- if skills:
- req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
-
- # if user wants to use skills in non-sandbox mode, apply local env tools
- runtime = self.skills_cfg.get("runtime", "local")
- sandbox_enabled = self.sandbox_cfg.get("enable", False)
- if runtime == "local" and not sandbox_enabled:
- self._apply_local_env_tools(req)
-
- # tools select
- tmgr = self.ctx.get_llm_tool_manager()
-
- # SubAgent orchestrator mode: main LLM only sees handoff tools.
- # NOTE: subagent_orchestrator config lives at top-level now.
- orch_cfg = self.ctx.get_config().get("subagent_orchestrator", {})
- if orch_cfg.get("main_enable", False):
- policy = str(orch_cfg.get("main_tools_policy", "handoff_only")).strip()
- if policy not in {"handoff_only", "unassigned_to_main"}:
- # Prefer the safer default when config contains unknown values.
- policy = "handoff_only"
-
- assigned_tools: set[str] = set()
- agents = orch_cfg.get("agents", [])
- if isinstance(agents, list):
- for a in agents:
- if not isinstance(a, dict):
- continue
- if a.get("enabled", True) is False:
- continue
- persona_tools = None
- persona_id = a.get("persona_id")
- if persona_id:
- persona_tools = next(
- (
- p.get("tools")
- for p in self.ctx.persona_manager.personas_v3
- if p["name"] == persona_id
- ),
- None,
- )
- tools = a.get("tools", [])
- if persona_tools is not None:
- tools = persona_tools
- if tools is None:
- assigned_tools.update(
- [
- tool.name
- for tool in tmgr.func_list
- if not isinstance(tool, HandoffTool)
- ]
- )
- continue
- if not isinstance(tools, list):
- continue
- for t in tools:
- name = str(t).strip()
- if name:
- assigned_tools.add(name)
-
- toolset = ToolSet()
-
- # Always expose handoff tools (transfer_to_*) when orchestrator is enabled.
- for tool in tmgr.func_list:
- if isinstance(tool, HandoffTool) and tool.active:
- toolset.add_tool(tool)
-
- # Optional mode: keep tools that are not assigned to any subagent on the main LLM.
- if policy == "unassigned_to_main":
- for tool in tmgr.func_list:
- if not tool.active:
- continue
- if isinstance(tool, HandoffTool):
- continue
- if tool.handler_module_path == "core.subagent_orchestrator":
- continue
- if tool.name in assigned_tools:
- continue
- toolset.add_tool(tool)
-
- # Override any earlier tool injection (e.g. skills local env tools) to keep
- # main-LLM tool visibility predictable under subagent orchestrator.
- req.func_tool = toolset
-
- # Encourage the model to delegate to subagents.
- # Use the built-in default router prompt; user overrides are disabled for now.
- router_prompt = (
- self.ctx.get_config()
- .get("subagent_orchestrator", {})
- .get("router_system_prompt", "")
- ).strip()
- if router_prompt:
- req.system_prompt += f"\n{router_prompt}\n"
-
- if policy == "unassigned_to_main":
- req.system_prompt += (
- "\n[Note: You may directly call the tools visible to the main LLM "
- "if they are not assigned to any subagent; otherwise prefer delegating "
- "to subagents via transfer_to_*.]\n"
- )
-
- return
-
- # Default behavior: follow persona tool selection.
- if (persona and persona.get("tools") is None) or not persona:
- # select all
- toolset = tmgr.get_full_tool_set()
- for tool in toolset:
- if not tool.active:
- toolset.remove_tool(tool.name)
- else:
- toolset = ToolSet()
- if persona["tools"]:
- for tool_name in persona["tools"]:
- tool = tmgr.get_func(tool_name)
- if tool and tool.active:
- toolset.add_tool(tool)
- if not req.func_tool:
- req.func_tool = toolset
- else:
- req.func_tool.merge(toolset)
- event.trace.record(
- "sel_persona", persona_id=persona_id, persona_toolset=toolset.names()
- )
- logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
-
- async def _ensure_img_caption(
- self,
- req: ProviderRequest,
- cfg: dict,
- img_cap_prov_id: str,
- ):
- try:
- caption = await self._request_img_caption(
- img_cap_prov_id,
- cfg,
- req.image_urls,
- )
- if caption:
- req.extra_user_content_parts.append(
- TextPart(text=f"{caption}")
- )
- req.image_urls = []
- except Exception as e:
- logger.error(f"处理图片描述失败: {e}")
-
- async def _request_img_caption(
- self,
- provider_id: str,
- cfg: dict,
- image_urls: list[str],
- ) -> str:
- if prov := self.ctx.get_provider_by_id(provider_id):
- if isinstance(prov, Provider):
- img_cap_prompt = cfg.get(
- "image_caption_prompt",
- "Please describe the image.",
- )
- logger.debug(f"Processing image caption with provider: {provider_id}")
- llm_resp = await prov.text_chat(
- prompt=img_cap_prompt,
- image_urls=image_urls,
- )
- return llm_resp.completion_text
- raise ValueError(
- f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.",
- )
- raise ValueError(
- f"Cannot get image caption because provider `{provider_id}` is not exist.",
- )
-
- async def process_llm_request(self, event: AstrMessageEvent, req: ProviderRequest):
- """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
- cfg: dict = self.ctx.get_config(umo=event.unified_msg_origin)[
- "provider_settings"
- ]
- self.skills_cfg = cfg.get("skills", {})
- self.sandbox_cfg = cfg.get("sandbox", {})
-
- # prompt prefix
- if prefix := cfg.get("prompt_prefix"):
- # 支持 {{prompt}} 作为用户输入的占位符
- if "{{prompt}}" in prefix:
- req.prompt = prefix.replace("{{prompt}}", req.prompt)
- else:
- req.prompt = prefix + req.prompt
-
- # 收集系统提醒信息
- system_parts = []
-
- # user identifier
- if cfg.get("identifier"):
- user_id = event.message_obj.sender.user_id
- user_nickname = event.message_obj.sender.nickname
- system_parts.append(f"User ID: {user_id}, Nickname: {user_nickname}")
-
- # group name identifier
- if cfg.get("group_name_display") and event.message_obj.group_id:
- if not event.message_obj.group:
- logger.error(
- f"Group name display enabled but group object is None. Group ID: {event.message_obj.group_id}"
- )
- return
- group_name = event.message_obj.group.group_name
- if group_name:
- system_parts.append(f"Group name: {group_name}")
-
- # time info
- if cfg.get("datetime_system_prompt"):
- current_time = None
- if self.timezone:
- # 启用时区
- try:
- now = datetime.datetime.now(zoneinfo.ZoneInfo(self.timezone))
- current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
- except Exception as e:
- logger.error(f"时区设置错误: {e}, 使用本地时区")
- if not current_time:
- current_time = (
- datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
- )
- system_parts.append(f"Current datetime: {current_time}")
-
- img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
- if req.conversation:
- # inject persona for this request
- platform_type = event.get_platform_name()
- await self._ensure_persona(
- req, cfg, event.unified_msg_origin, platform_type, event
- )
-
- # image caption
- if img_cap_prov_id and req.image_urls:
- await self._ensure_img_caption(req, cfg, img_cap_prov_id)
-
- # quote message processing
- # 解析引用内容
- quote = None
- for comp in event.message_obj.message:
- if isinstance(comp, Reply):
- quote = comp
- break
- if quote:
- content_parts = []
-
- # 1. 处理引用的文本
- sender_info = (
- f"({quote.sender_nickname}): " if quote.sender_nickname else ""
- )
- message_str = quote.message_str or "[Empty Text]"
- content_parts.append(f"{sender_info}{message_str}")
-
- # 2. 处理引用的图片 (保留原有逻辑,但改变输出目标)
- image_seg = None
- if quote.chain:
- for comp in quote.chain:
- if isinstance(comp, Image):
- image_seg = comp
- break
-
- if image_seg:
- try:
- # 找到可以生成图片描述的 provider
- prov = None
- if img_cap_prov_id:
- prov = self.ctx.get_provider_by_id(img_cap_prov_id)
- if prov is None:
- prov = self.ctx.get_using_provider(event.unified_msg_origin)
-
- # 调用 provider 生成图片描述
- if prov and isinstance(prov, Provider):
- llm_resp = await prov.text_chat(
- prompt="Please describe the image content.",
- image_urls=[await image_seg.convert_to_file_path()],
- )
- if llm_resp.completion_text:
- # 将图片描述作为文本添加到 content_parts
- content_parts.append(
- f"[Image Caption in quoted message]: {llm_resp.completion_text}"
- )
- else:
- logger.warning(
- "No provider found for image captioning in quote."
- )
- except BaseException as e:
- logger.error(f"处理引用图片失败: {e}")
-
- # 3. 将所有部分组合成文本并添加到 extra_user_content_parts 中
- # 确保引用内容被正确的标签包裹
- quoted_content = "\n".join(content_parts)
- # 确保所有内容都在标签内
- quoted_text = f"\n{quoted_content}\n"
-
- req.extra_user_content_parts.append(TextPart(text=quoted_text))
-
- # 统一包裹所有系统提醒
- if system_parts:
- system_content = (
- "" + "\n".join(system_parts) + ""
- )
- req.extra_user_content_parts.append(TextPart(text=system_content))
diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py
index 91f8b3e21..d238f757b 100644
--- a/astrbot/core/astr_agent_tool_exec.py
+++ b/astrbot/core/astr_agent_tool_exec.py
@@ -3,6 +3,7 @@ import inspect
import traceback
import typing as T
import uuid
+import json
import mcp
@@ -20,9 +21,13 @@ from astrbot.core.message.message_event_result import (
MessageChain,
MessageEventResult,
)
+from astrbot.core.provider.entites import ProviderRequest
from astrbot.core.platform.message_session import MessageSession
from astrbot.core.provider.register import llm_tools
-from astrbot.core.message.components import Plain
+from astrbot.core.astr_main_agent_resources import (
+ BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
+ SEND_MESSAGE_TO_USER_TOOL,
+)
class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
@@ -148,7 +153,11 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
task_id: str,
**tool_args,
):
- from astrbot.core.astr_main_agent import build_main_agent, MainAgentBuildConfig
+ from astrbot.core.astr_main_agent import (
+ build_main_agent,
+ MainAgentBuildConfig,
+ _get_session_conv,
+ )
# run the tool
result_text = ""
@@ -191,47 +200,47 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
message_type=session.message_type,
)
config = MainAgentBuildConfig(tool_call_timeout=3600)
- result = await build_main_agent(
- event=cron_event, plugin_context=ctx, config=config
- )
- if not result:
- logger.error("Failed to build main agent for cron job.")
- return
- runner = result.agent_runner
- req = result.provider_request
- bg = extras["background_task_result"]
- result_text = bg["result"] or "Empty Response"
- if req.contexts:
+ req = ProviderRequest()
+ conv = await _get_session_conv(event=cron_event, plugin_context=ctx)
+ req.conversation = conv
+ context = json.loads(conv.history)
+ if context:
+ req.contexts = context
context_dump = req._print_friendly_context()
+ req.contexts = []
req.system_prompt += (
"\n\nBellow is you and user previous conversation history:\n"
f"{context_dump}"
)
- req.system_prompt += (
- "You now have a new background task result:\n"
- f"- Task ID: {bg['task_id']}\n"
- f"- Executed Tool: {tool.name}\n"
- f"- Tool Args: {tool_args}\n"
- f"- Result: {result_text}\n"
- f"- Note: {note}\n"
- "Please tell the user the result of the background task in your next response."
- )
+ bg = json.dumps(extras["background_task_result"], ensure_ascii=False)
+ req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
+ background_task_result=bg
+ )
req.prompt = (
- "You have a new background task result to report to the user."
- " Please include the result in your next response."
- " Using same language as previous conversation."
+ "Proceed according to your system instructions. "
+ "Output using same language as previous conversation."
)
+ if not req.func_tool:
+ req.func_tool = ToolSet()
+ req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)
+ result = await build_main_agent(
+ event=cron_event, plugin_context=ctx, config=config, req=req
+ )
+ if not result:
+ logger.error("Failed to build main agent for background task job.")
+ return
+
+ runner = result.agent_runner
async for _ in runner.step_until_done(30):
+ # agent will send message to user via using tools
pass
llm_resp = runner.get_final_llm_resp()
if not llm_resp:
- logger.warning("Cron job agent got no response")
+ logger.warning("background task agent got no response")
return
- message_chain = MessageChain(chain=[Plain(text=llm_resp.completion_text)])
- await ctx.send_message(session=session, message_chain=message_chain)
@classmethod
async def _execute_local(
diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py
index b35bd971b..e58625bf1 100644
--- a/astrbot/core/astr_main_agent.py
+++ b/astrbot/core/astr_main_agent.py
@@ -1,22 +1,46 @@
from __future__ import annotations
import asyncio
+import builtins
+import copy
+import datetime
import json
import os
+import zoneinfo
from dataclasses import dataclass, field
+from astrbot.api import sp
from astrbot.core import logger
+from astrbot.core.agent.handoff import HandoffTool
from astrbot.core.agent.message import TextPart
from astrbot.core.agent.tool import ToolSet
from astrbot.core.astr_agent_context import AgentContextWrapper, AstrAgentContext
from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS
from astrbot.core.astr_agent_run_util import AgentRunner
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
+from astrbot.core.astr_main_agent_resources import (
+ CHATUI_EXTRA_PROMPT,
+ CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
+ EXECUTE_SHELL_TOOL,
+ FILE_DOWNLOAD_TOOL,
+ FILE_UPLOAD_TOOL,
+ KNOWLEDGE_BASE_QUERY_TOOL,
+ LIVE_MODE_SYSTEM_PROMPT,
+ LOCAL_EXECUTE_SHELL_TOOL,
+ LOCAL_PYTHON_TOOL,
+ LLM_SAFETY_MODE_SYSTEM_PROMPT,
+ PYTHON_TOOL,
+ SANDBOX_MODE_PROMPT,
+ TOOL_CALL_PROMPT,
+ TOOL_CALL_PROMPT_SKILLS_LIKE_MODE,
+ retrieve_knowledge_base,
+)
from astrbot.core.conversation_mgr import Conversation
from astrbot.core.message.components import File, Image, Reply
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider import Provider
from astrbot.core.provider.entities import ProviderRequest
+from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
from astrbot.core.star.context import Context
from astrbot.core.star.star_handler import star_map
from astrbot.core.tools.cron_tools import (
@@ -27,42 +51,59 @@ from astrbot.core.tools.cron_tools import (
from astrbot.core.utils.file_extract import extract_file_moonshotai
from astrbot.core.utils.llm_metadata import LLM_METADATAS
-from .astr_main_agent_resources import (
- CHATUI_EXTRA_PROMPT,
- EXECUTE_SHELL_TOOL,
- FILE_DOWNLOAD_TOOL,
- FILE_UPLOAD_TOOL,
- KNOWLEDGE_BASE_QUERY_TOOL,
- LIVE_MODE_SYSTEM_PROMPT,
- LLM_SAFETY_MODE_SYSTEM_PROMPT,
- PYTHON_TOOL,
- SANDBOX_MODE_PROMPT,
- TOOL_CALL_PROMPT,
- TOOL_CALL_PROMPT_SKILLS_LIKE_MODE,
- retrieve_knowledge_base,
-)
-
@dataclass(slots=True)
class MainAgentBuildConfig:
+ """The main agent build configuration.
+ Most of the configs can be found in the cmd_config.json"""
+
tool_call_timeout: int
+ """The timeout (in seconds) for a tool call.
+ When the tool call exceeds this time,
+ a timeout error as a tool result will be returned.
+ """
tool_schema_mode: str = "full"
+ """The tool schema mode, can be 'full' or 'skills-like'."""
provider_wake_prefix: str = ""
+ """The wake prefix for the provider. If the user message does not start with this prefix,
+ the main agent will not be triggered."""
streaming_response: bool = True
+ """Whether to use streaming response."""
sanitize_context_by_modalities: bool = False
+ """Whether to sanitize the context based on the provider's supported modalities.
+ This will remove unsupported message types(e.g. image) from the context to prevent issues."""
kb_agentic_mode: bool = False
+ """Whether to use agentic mode for knowledge base retrieval.
+ This will inject the knowledge base query tool into the main agent's toolset to allow dynamic querying."""
file_extract_enabled: bool = False
+ """Whether to enable file content extraction for uploaded files."""
file_extract_prov: str = "moonshotai"
+ """The file extraction provider."""
file_extract_msh_api_key: str = ""
+ """The API key for Moonshot AI file extraction provider."""
context_limit_reached_strategy: str = "truncate_by_turns"
+ """The strategy to handle context length limit reached."""
llm_compress_instruction: str = ""
- llm_compress_keep_recent: int = 4
+ """The instruction for compression in llm_compress strategy."""
+ llm_compress_keep_recent: int = 6
+ """The number of most recent turns to keep during llm_compress strategy."""
llm_compress_provider_id: str = ""
- max_context_length: int = 0
+ """The provider ID for the LLM used in context compression."""
+ max_context_length: int = -1
+ """The maximum number of turns to keep in context. -1 means no limit.
+ This enforce max turns before compression"""
dequeue_context_length: int = 1
+ """The number of oldest turns to remove when context length limit is reached."""
llm_safety_mode: bool = True
+ """This will inject healthy and safe system prompt into the main agent,
+ to prevent LLM output harmful information"""
safety_mode_strategy: str = "system_prompt"
sandbox_cfg: dict = field(default_factory=dict)
+ add_cron_tools: bool = True
+ """This will add cron job management tools to the main agent for proactive cron job execution."""
+ provider_settings: dict = field(default_factory=dict)
+ subagent_orchestrator: dict = field(default_factory=dict)
+ timezone: str | None = None
@dataclass(slots=True)
@@ -189,6 +230,388 @@ async def _apply_file_extract(
)
+def _apply_prompt_prefix(req: ProviderRequest, cfg: dict) -> None:
+ prefix = cfg.get("prompt_prefix")
+ if not prefix:
+ return
+ if "{{prompt}}" in prefix:
+ req.prompt = prefix.replace("{{prompt}}", req.prompt)
+ else:
+ req.prompt = f"{prefix}{req.prompt}"
+
+
+def _apply_local_env_tools(req: ProviderRequest) -> None:
+ if req.func_tool is None:
+ req.func_tool = ToolSet()
+ req.func_tool.add_tool(LOCAL_EXECUTE_SHELL_TOOL)
+ req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
+
+
+async def _ensure_persona_and_skills(
+ req: ProviderRequest,
+ cfg: dict,
+ plugin_context: Context,
+ event: AstrMessageEvent,
+) -> None:
+ """Ensure persona and skills are applied to the request's system prompt or user prompt."""
+ if not req.conversation:
+ return
+
+ # get persona ID
+ persona_id = (
+ await sp.get_async(
+ scope="umo",
+ scope_id=event.unified_msg_origin,
+ key="session_service_config",
+ default={},
+ )
+ ).get("persona_id")
+
+ if not persona_id:
+ persona_id = req.conversation.persona_id or cfg.get("default_personality")
+ if persona_id is None or persona_id != "[%None]":
+ default_persona = plugin_context.persona_manager.selected_default_persona_v3
+ if default_persona:
+ persona_id = default_persona["name"]
+ if event.get_platform_name() == "webchat":
+ persona_id = "_chatui_default_"
+ req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
+
+ persona = next(
+ builtins.filter(
+ lambda persona: persona["name"] == persona_id,
+ plugin_context.persona_manager.personas_v3,
+ ),
+ None,
+ )
+ if persona:
+ # Inject persona system prompt
+ if prompt := persona["prompt"]:
+ req.system_prompt += f"\n# Persona Instructions\n\n{prompt}\n"
+ if begin_dialogs := copy.deepcopy(persona.get("_begin_dialogs_processed")):
+ req.contexts[:0] = begin_dialogs
+
+ # Inject skills prompt
+ skills_cfg = cfg.get("skills", {})
+ sandbox_cfg = cfg.get("sandbox", {})
+ skill_manager = SkillManager()
+ runtime = skills_cfg.get("runtime", "local")
+ skills = skill_manager.list_skills(active_only=True, runtime=runtime)
+
+ if runtime == "sandbox" and not sandbox_cfg.get("enable", False):
+ logger.warning(
+ "Skills runtime is set to sandbox, but sandbox mode is disabled, will skip skills prompt injection.",
+ )
+ req.system_prompt += (
+ "\n[Background: User added some skills, and skills runtime is set to sandbox, "
+ "but sandbox mode is disabled. So skills will be unavailable.]\n"
+ )
+ elif skills:
+ if persona and persona.get("skills") is not None:
+ if not persona["skills"]:
+ skills = []
+ else:
+ allowed = set(persona["skills"])
+ skills = [skill for skill in skills if skill.name in allowed]
+ if skills:
+ req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
+
+ runtime = skills_cfg.get("runtime", "local")
+ sandbox_enabled = sandbox_cfg.get("enable", False)
+ if runtime == "local" and not sandbox_enabled:
+ _apply_local_env_tools(req)
+
+ tmgr = plugin_context.get_llm_tool_manager()
+
+ orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {})
+ if orch_cfg.get("main_enable", False):
+ policy = str(orch_cfg.get("main_tools_policy", "handoff_only")).strip()
+ if policy not in {"handoff_only", "unassigned_to_main"}:
+ policy = "handoff_only"
+
+ assigned_tools: set[str] = set()
+ agents = orch_cfg.get("agents", [])
+ if isinstance(agents, list):
+ for a in agents:
+ if not isinstance(a, dict):
+ continue
+ if a.get("enabled", True) is False:
+ continue
+ persona_tools = None
+ pid = a.get("persona_id")
+ if pid:
+ persona_tools = next(
+ (
+ p.get("tools")
+ for p in plugin_context.persona_manager.personas_v3
+ if p["name"] == pid
+ ),
+ None,
+ )
+ tools = a.get("tools", [])
+ if persona_tools is not None:
+ tools = persona_tools
+ if tools is None:
+ assigned_tools.update(
+ [
+ tool.name
+ for tool in tmgr.func_list
+ if not isinstance(tool, HandoffTool)
+ ]
+ )
+ continue
+ if not isinstance(tools, list):
+ continue
+ for t in tools:
+ name = str(t).strip()
+ if name:
+ assigned_tools.add(name)
+
+ toolset = ToolSet()
+ for tool in tmgr.func_list:
+ if isinstance(tool, HandoffTool) and tool.active:
+ toolset.add_tool(tool)
+
+ if policy == "unassigned_to_main":
+ for tool in tmgr.func_list:
+ if not tool.active:
+ continue
+ if isinstance(tool, HandoffTool):
+ continue
+ if tool.handler_module_path == "core.subagent_orchestrator":
+ continue
+ if tool.name in assigned_tools:
+ continue
+ toolset.add_tool(tool)
+
+ req.func_tool = toolset
+
+ router_prompt = (
+ plugin_context.get_config()
+ .get("subagent_orchestrator", {})
+ .get("router_system_prompt", "")
+ ).strip()
+ if router_prompt:
+ req.system_prompt += f"\n{router_prompt}\n"
+ if policy == "unassigned_to_main":
+ req.system_prompt += (
+ "\n[Note: You may directly call the tools visible to the main LLM "
+ "if they are not assigned to any subagent; otherwise prefer delegating "
+ "to subagents via transfer_to_*.]\n"
+ )
+ return
+
+ # inject toolset in the persona
+ if (persona and persona.get("tools") is None) or not persona:
+ toolset = tmgr.get_full_tool_set()
+ for tool in list(toolset):
+ if not tool.active:
+ toolset.remove_tool(tool.name)
+ else:
+ toolset = ToolSet()
+ if persona["tools"]:
+ for tool_name in persona["tools"]:
+ tool = tmgr.get_func(tool_name)
+ if tool and tool.active:
+ toolset.add_tool(tool)
+ if not req.func_tool:
+ req.func_tool = toolset
+ else:
+ req.func_tool.merge(toolset)
+ try:
+ event.trace.record(
+ "sel_persona", persona_id=persona_id, persona_toolset=toolset.names()
+ )
+ except Exception:
+ pass
+ logger.debug("Tool set for persona %s: %s", persona_id, toolset.names())
+
+
+async def _request_img_caption(
+ provider_id: str,
+ cfg: dict,
+ image_urls: list[str],
+ plugin_context: Context,
+) -> str:
+ prov = plugin_context.get_provider_by_id(provider_id)
+ if prov is None:
+ raise ValueError(
+ f"Cannot get image caption because provider `{provider_id}` is not exist.",
+ )
+ if not isinstance(prov, Provider):
+ raise ValueError(
+ f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.",
+ )
+
+ img_cap_prompt = cfg.get(
+ "image_caption_prompt",
+ "Please describe the image.",
+ )
+ logger.debug("Processing image caption with provider: %s", provider_id)
+ llm_resp = await prov.text_chat(
+ prompt=img_cap_prompt,
+ image_urls=image_urls,
+ )
+ return llm_resp.completion_text
+
+
+async def _ensure_img_caption(
+ req: ProviderRequest,
+ cfg: dict,
+ plugin_context: Context,
+ image_caption_provider: str,
+) -> None:
+ try:
+ caption = await _request_img_caption(
+ image_caption_provider,
+ cfg,
+ req.image_urls,
+ plugin_context,
+ )
+ if caption:
+ req.extra_user_content_parts.append(
+ TextPart(text=f"{caption}")
+ )
+ req.image_urls = []
+ except Exception as exc: # noqa: BLE001
+ logger.error("处理图片描述失败: %s", exc)
+
+
+async def _process_quote_message(
+ event: AstrMessageEvent,
+ req: ProviderRequest,
+ img_cap_prov_id: str,
+ plugin_context: Context,
+) -> None:
+ quote = None
+ for comp in event.message_obj.message:
+ if isinstance(comp, Reply):
+ quote = comp
+ break
+ if not quote:
+ return
+
+ content_parts = []
+ sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else ""
+ message_str = quote.message_str or "[Empty Text]"
+ content_parts.append(f"{sender_info}{message_str}")
+
+ image_seg = None
+ if quote.chain:
+ for comp in quote.chain:
+ if isinstance(comp, Image):
+ image_seg = comp
+ break
+
+ if image_seg:
+ try:
+ prov = None
+ if img_cap_prov_id:
+ prov = plugin_context.get_provider_by_id(img_cap_prov_id)
+ if prov is None:
+ prov = plugin_context.get_using_provider(event.unified_msg_origin)
+
+ if prov and isinstance(prov, Provider):
+ llm_resp = await prov.text_chat(
+ prompt="Please describe the image content.",
+ image_urls=[await image_seg.convert_to_file_path()],
+ )
+ if llm_resp.completion_text:
+ content_parts.append(
+ f"[Image Caption in quoted message]: {llm_resp.completion_text}"
+ )
+ else:
+ logger.warning("No provider found for image captioning in quote.")
+ except BaseException as exc:
+ logger.error("处理引用图片失败: %s", exc)
+
+ quoted_content = "\n".join(content_parts)
+ quoted_text = f"\n{quoted_content}\n"
+ req.extra_user_content_parts.append(TextPart(text=quoted_text))
+
+
+def _append_system_reminders(
+ event: AstrMessageEvent,
+ req: ProviderRequest,
+ cfg: dict,
+ timezone: str | None,
+) -> None:
+ system_parts: list[str] = []
+ if cfg.get("identifier"):
+ user_id = event.message_obj.sender.user_id
+ user_nickname = event.message_obj.sender.nickname
+ system_parts.append(f"User ID: {user_id}, Nickname: {user_nickname}")
+
+ if cfg.get("group_name_display") and event.message_obj.group_id:
+ if not event.message_obj.group:
+ logger.error(
+ "Group name display enabled but group object is None. Group ID: %s",
+ event.message_obj.group_id,
+ )
+ else:
+ group_name = event.message_obj.group.group_name
+ if group_name:
+ system_parts.append(f"Group name: {group_name}")
+
+ if cfg.get("datetime_system_prompt"):
+ current_time = None
+ if timezone:
+ try:
+ now = datetime.datetime.now(zoneinfo.ZoneInfo(timezone))
+ current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
+ except Exception as exc: # noqa: BLE001
+ logger.error("时区设置错误: %s, 使用本地时区", exc)
+ if not current_time:
+ current_time = (
+ datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
+ )
+ system_parts.append(f"Current datetime: {current_time}")
+
+ if system_parts:
+ system_content = (
+ "" + "\n".join(system_parts) + ""
+ )
+ req.extra_user_content_parts.append(TextPart(text=system_content))
+
+
+async def _decorate_llm_request(
+ event: AstrMessageEvent,
+ req: ProviderRequest,
+ plugin_context: Context,
+ config: MainAgentBuildConfig,
+) -> None:
+ cfg = config.provider_settings or plugin_context.get_config(
+ umo=event.unified_msg_origin
+ ).get("provider_settings", {})
+
+ _apply_prompt_prefix(req, cfg)
+
+ if req.conversation:
+ await _ensure_persona_and_skills(req, cfg, plugin_context, event)
+
+ img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
+ if img_cap_prov_id and req.image_urls:
+ await _ensure_img_caption(
+ req,
+ cfg,
+ plugin_context,
+ img_cap_prov_id,
+ )
+
+ img_cap_prov_id = cfg.get("default_image_caption_provider_id") or ""
+ await _process_quote_message(
+ event,
+ req,
+ img_cap_prov_id,
+ plugin_context,
+ )
+
+ tz = config.timezone
+ if tz is None:
+ tz = plugin_context.get_config().get("timezone")
+ _append_system_reminders(event, req, cfg, tz)
+
+
def _modalities_fix(provider: Provider, req: ProviderRequest) -> None:
if req.image_urls:
provider_cfg = provider.provider_config.get("modalities", ["image"])
@@ -373,7 +796,7 @@ def _apply_sandbox_tools(
req.system_prompt += f"\n{SANDBOX_MODE_PROMPT}\n"
-def _proactive_cron_job_tools(req: ProviderRequest, event: AstrMessageEvent) -> None:
+def _proactive_cron_job_tools(req: ProviderRequest) -> None:
if req.func_tool is None:
req.func_tool = ToolSet()
req.func_tool.add_tool(CREATE_CRON_JOB_TOOL)
@@ -474,6 +897,8 @@ async def build_main_agent(
else:
return None
+ await _decorate_llm_request(event, req, plugin_context, config)
+
await _apply_kb(event, req, plugin_context, config)
if not req.session_id:
@@ -495,7 +920,8 @@ async def build_main_agent(
event=event,
)
- _proactive_cron_job_tools(req, event)
+ if config.add_cron_tools:
+ _proactive_cron_job_tools(req)
if provider.provider_config.get("max_context_tokens", 0) <= 0:
model = provider.get_model()
diff --git a/astrbot/core/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py
index 10554cbae..37bf318e3 100644
--- a/astrbot/core/astr_main_agent_resources.py
+++ b/astrbot/core/astr_main_agent_resources.py
@@ -41,11 +41,12 @@ SANDBOX_MODE_PROMPT = (
)
TOOL_CALL_PROMPT = (
- "You MUST NOT return an empty response, especially after invoking a tool."
- " Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
- " Use the provided tool schema to format arguments and do not guess parameters that are not defined."
- " After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
- " Keep the role-play and style consistent throughout the conversation."
+ "When using tools: "
+ "never return an empty response; "
+ "briefly explain the purpose before calling a tool; "
+ "follow the tool schema exactly and do not invent parameters; "
+ "after execution, briefly summarize the result for the user; "
+ "keep the conversation style consistent."
)
TOOL_CALL_PROMPT_SKILLS_LIKE_MODE = (
@@ -91,6 +92,43 @@ LIVE_MODE_SYSTEM_PROMPT = (
"Sound like a real conversation, not a Q&A system."
)
+PROACTIVE_AGENT_CRON_WOKE_SYSTEM_PROMPT = (
+ "You are an autonomous proactive agent.\n\n"
+ "You are awakened by a scheduled cron job, not by a user message.\n"
+ "You are given:"
+ "1. A cron job description explaining why you are activated.\n"
+ "2. Historical conversation context between you and the user.\n"
+ "3. Your available tools and skills.\n"
+ "# IMPORTANT RULES\n"
+ "1. This is NOT a chat turn. Do NOT greet the user. Do NOT ask the user questions unless strictly necessary.\n"
+ "2. Use historical conversation and memory to understand you and user's relationship, preferences, and context.\n"
+ "3. If messaging the user: Explain WHY you are contacting them; Reference the cron task implicitly (not technical details).\n"
+ "4. You can use your available tools and skills to finish the task if needed.\n"
+ "5. Use `send_message_to_user` tool to send message to user if needed."
+ "# CRON JOB CONTEXT\n"
+ "The following object describes the scheduled task that triggered you:\n"
+ "{cron_job}"
+)
+
+BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT = (
+ "You are an autonomous proactive agent.\n\n"
+ "You are awakened by the completion of a background task you initiated earlier.\n"
+ "You are given:"
+ "1. A description of the background task you initiated.\n"
+ "2. The result of the background task.\n"
+ "3. Historical conversation context between you and the user.\n"
+ "4. Your available tools and skills.\n"
+ "# IMPORTANT RULES\n"
+ "1. This is NOT a chat turn. Do NOT greet the user. Do NOT ask the user questions unless strictly necessary. Do NOT respond if no meaningful action is required."
+ "2. Use historical conversation and memory to understand you and user's relationship, preferences, and context."
+ "3. If messaging the user: Explain WHY you are contacting them; Reference the background task implicitly (not technical details)."
+ "4. You can use your available tools and skills to finish the task if needed.\n"
+ "5. Use `send_message_to_user` tool to send message to user if needed."
+ "# BACKGROUND TASK CONTEXT\n"
+ "The following object describes the background task that completed:\n"
+ "{background_task_result}"
+)
+
@dataclass
class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]):
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index d911cd447..702316d2e 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -91,7 +91,7 @@ DEFAULT_CONFIG = {
"3. If there was an initial user goal, state it first and describe the current progress/status.\n"
"4. Write the summary in the user's language.\n"
),
- "llm_compress_keep_recent": 4,
+ "llm_compress_keep_recent": 6,
"llm_compress_provider_id": "",
"max_context_length": -1,
"dequeue_context_length": 1,
diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py
index a877d45ce..64b3e2a21 100644
--- a/astrbot/core/cron/manager.py
+++ b/astrbot/core/cron/manager.py
@@ -1,4 +1,5 @@
import asyncio
+import json
from datetime import datetime, timezone
from typing import Any, Awaitable, Callable
from zoneinfo import ZoneInfo
@@ -7,12 +8,12 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from astrbot import logger
+from astrbot.core.agent.tool import ToolSet
from astrbot.core.cron.events import CronMessageEvent
from astrbot.core.db import BaseDatabase
from astrbot.core.db.po import CronJob
from astrbot.core.platform.message_session import MessageSession
-from astrbot.core.message.message_event_result import MessageChain
-from astrbot.core.message.components import Plain
+from astrbot.core.provider.entites import ProviderRequest
from typing import TYPE_CHECKING
@@ -239,7 +240,16 @@ class CronJobManager:
session_str: str,
extras: dict,
):
- from astrbot.core.astr_main_agent import build_main_agent, MainAgentBuildConfig
+ """Woke the main agent to handle the cron job message."""
+ from astrbot.core.astr_main_agent import (
+ build_main_agent,
+ MainAgentBuildConfig,
+ _get_session_conv,
+ )
+ from astrbot.core.astr_main_agent_resources import (
+ PROACTIVE_AGENT_CRON_WOKE_SYSTEM_PROMPT,
+ SEND_MESSAGE_TO_USER_TOOL,
+ )
try:
session = (
@@ -259,43 +269,53 @@ class CronJobManager:
message_type=session.message_type,
)
- config = MainAgentBuildConfig(tool_call_timeout=3600)
+ config = MainAgentBuildConfig(
+ tool_call_timeout=3600,
+ llm_safety_mode=False,
+ )
+ req = ProviderRequest()
+ conv = await _get_session_conv(event=cron_event, plugin_context=self.ctx)
+ req.conversation = conv
+ # finetine the messages
+ context = json.loads(conv.history)
+ if context:
+ req.contexts = context
+ context_dump = req._print_friendly_context()
+ req.contexts = []
+ req.system_prompt += (
+ "\n\nBellow is you and user previous conversation history:\n"
+ f"---\n"
+ f"{context_dump}\n"
+ f"---\n"
+ )
+ cron_job_str = json.dumps(extras.get("cron_job", {}), ensure_ascii=False)
+ req.system_prompt += PROACTIVE_AGENT_CRON_WOKE_SYSTEM_PROMPT.format(
+ cron_job=cron_job_str
+ )
+ req.prompt = (
+ "You are now responding to a scheduled task"
+ "Proceed according to your system instructions. "
+ "Output using same language as previous conversation."
+ )
+ if not req.func_tool:
+ req.func_tool = ToolSet()
+ req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)
+
result = await build_main_agent(
- event=cron_event, plugin_context=self.ctx, config=config
+ event=cron_event, plugin_context=self.ctx, config=config, req=req
)
if not result:
logger.error("Failed to build main agent for cron job.")
return
- req = result.provider_request
+
runner = result.agent_runner
-
- # finetine the messages
- job_name = extras.get("name", "scheduled task")
- note = extras.get("note") or extras.get("description") or ""
- if req.contexts:
- context_dump = req._print_friendly_context()
- req.system_prompt += (
- "\n\nBellow is you and user previous conversation history:\n"
- f"{context_dump}"
- )
- req.system_prompt += (
- "\n[Scheduler Context] This turn is triggered automatically by cron job "
- f'"{job_name}" (type: {extras.get("type", "unknown")}). '
- "Act proactively based on the provided note and current context. "
- )
- if note:
- req.system_prompt += f"[Scheduler Note]: {note}\n"
-
- req.prompt = "You are now responding to a scheduled task. Output using same language as previous conversation."
-
async for _ in runner.step_until_done(30):
+ # agent will send message to user via using tools
pass
llm_resp = runner.get_final_llm_resp()
if not llm_resp:
logger.warning("Cron job agent got no response")
return
- message_chain = MessageChain(chain=[Plain(text=llm_resp.completion_text)])
- await self.ctx.send_message(session=session, message_chain=message_chain)
__all__ = ["CronJobManager"]
diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
index 7e218f637..f67164821 100644
--- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
+++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
@@ -113,6 +113,9 @@ class InternalAgentSubStage(Stage):
llm_safety_mode=self.llm_safety_mode,
safety_mode_strategy=self.safety_mode_strategy,
sandbox_cfg=self.sandbox_cfg,
+ provider_settings=settings,
+ subagent_orchestrator=conf.get("subagent_orchestrator", {}),
+ timezone=self.ctx.plugin_manager.context.get_config().get("timezone"),
)
async def process(
diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py
index 857a19181..c4259aebd 100644
--- a/astrbot/core/tools/cron_tools.py
+++ b/astrbot/core/tools/cron_tools.py
@@ -8,10 +8,10 @@ from astrbot.core.astr_agent_context import AstrAgentContext
@dataclass
class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
- name: str = "create_cron_job"
+ name: str = "create_future_task"
description: str = (
- "Create a scheduled active agent task using a cron expression. "
- "Use this when the user asks for recurring tasks (e.g., daily reports)."
+ "Create a future task for your future using a cron expression. "
+ "Use this when you or the user want recurring follow-up (e.g., daily report to self)."
)
parameters: dict = Field(
default_factory=lambda: {
@@ -19,15 +19,15 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
"properties": {
"cron_expression": {
"type": "string",
- "description": "Cron expression defining when to trigger (e.g., '0 8 * * *').",
+ "description": "Cron expression defining when your future agent should wake (e.g., '0 8 * * *').",
},
"note": {
"type": "string",
- "description": "Instruction for the future agent run when the job triggers.",
+ "description": "Detailed instructions for your future agent to execute when it wakes.",
},
"name": {
"type": "string",
- "description": "Optional job name for identification.",
+ "description": "Optional label to recognize this future task.",
},
},
"required": ["cron_expression", "note"],
@@ -61,15 +61,15 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
)
next_run = job.next_run_time
return (
- f"Scheduled cron job {job.job_id} ({job.name}) with expression '{cron_expression}'. "
- f"Next run: {next_run}"
+ f"Scheduled future task {job.job_id} ({job.name}) with expression '{cron_expression}'. "
+ f"Your future agent will wake at: {next_run}"
)
@dataclass
class DeleteCronJobTool(FunctionTool[AstrAgentContext]):
- name: str = "delete_cron_job"
- description: str = "Delete a cron job by its job_id."
+ name: str = "delete_future_task"
+ description: str = "Delete a future task (cron job) by its job_id."
parameters: dict = Field(
default_factory=lambda: {
"type": "object",
@@ -98,8 +98,8 @@ class DeleteCronJobTool(FunctionTool[AstrAgentContext]):
@dataclass
class ListCronJobsTool(FunctionTool[AstrAgentContext]):
- name: str = "list_cron_jobs"
- description: str = "List existing cron jobs for inspection."
+ name: str = "list_future_tasks"
+ description: str = "List existing future tasks (cron jobs) for inspection."
parameters: dict = Field(
default_factory=lambda: {
"type": "object",