Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions astrbot/core/agent/context/compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ def __init__(
provider: "Provider",
keep_recent: int = 4,
instruction_text: str | None = None,
user_prompt: str | None = None,
ack_prompt: str | None = None,
compression_threshold: float = 0.82,
) -> None:
"""Initialize the LLM summary compressor.
Expand All @@ -173,6 +175,16 @@ def __init__(
"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"
)
PLACEHOLDER = "{summary_content}"
self.usr_prompt = (
user_prompt
if user_prompt and user_prompt.count(PLACEHOLDER) == 1
else f"Our previous history conversation summary: {PLACEHOLDER}"
)
self.ack_prompt = (
ack_prompt
or "Acknowledged the summary of our previous conversation history."
)

def should_compress(
self, messages: list[Message], current_tokens: int, max_tokens: int
Expand Down Expand Up @@ -229,13 +241,13 @@ async def __call__(self, messages: list[Message]) -> list[Message]:
result.append(
Message(
role="user",
content=f"Our previous history conversation summary: {summary_content}",
content=self.usr_prompt.format(summary_content=summary_content),
)
)
result.append(
Message(
role="assistant",
content="Acknowledged the summary of our previous conversation history.",
content=self.ack_prompt,
)
)

Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/agent/context/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class ContextConfig:
"""
llm_compress_instruction: str | None = None
"""Instruction prompt for LLM-based compression."""
context_summary_user_prompt: str | None = None
"""User prompt for context summarization when using LLM-based compression."""
context_summary_ack_prompt: str | None = None
"""Assistant prompt for context summarization when using LLM-based compression."""
llm_compress_keep_recent: int = 0
"""Number of recent messages to keep during LLM-based compression."""
llm_compress_provider: "Provider | None" = None
Expand Down
2 changes: 2 additions & 0 deletions astrbot/core/agent/context/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def __init__(
provider=config.llm_compress_provider,
keep_recent=config.llm_compress_keep_recent,
instruction_text=config.llm_compress_instruction,
user_prompt=config.context_summary_user_prompt,
ack_prompt=config.context_summary_ack_prompt,
)
else:
self.compressor = TruncateByTurnsCompressor(
Expand Down
61 changes: 48 additions & 13 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ async def reset(
enforce_max_turns: int = -1,
# llm compressor
llm_compress_instruction: str | None = None,
context_summary_user_prompt: str | None = None,
context_summary_ack_prompt: str | None = None,
llm_compress_keep_recent: int = 0,
llm_compress_provider: Provider | None = None,
# truncate by turns compressor
Expand All @@ -108,13 +110,18 @@ async def reset(
custom_token_counter: TokenCounter | None = None,
custom_compressor: ContextCompressor | None = None,
tool_schema_mode: str | None = "full",
tool_call_requery_instruction_prompt: str = "",
tool_call_follow_up_notice_prompt: str = "",
tool_call_max_step_reached_prompt: str = "",
fallback_providers: list[Provider] | None = None,
**kwargs: T.Any,
) -> None:
self.req = request
self.streaming = streaming
self.enforce_max_turns = enforce_max_turns
self.llm_compress_instruction = llm_compress_instruction
self.context_summary_user_prompt = context_summary_user_prompt
self.context_summary_ack_prompt = context_summary_ack_prompt
self.llm_compress_keep_recent = llm_compress_keep_recent
self.llm_compress_provider = llm_compress_provider
self.truncate_turns = truncate_turns
Expand All @@ -130,6 +137,8 @@ async def reset(
enforce_max_turns=self.enforce_max_turns,
truncate_turns=self.truncate_turns,
llm_compress_instruction=self.llm_compress_instruction,
context_summary_user_prompt=self.context_summary_user_prompt,
context_summary_ack_prompt=self.context_summary_ack_prompt,
llm_compress_keep_recent=self.llm_compress_keep_recent,
llm_compress_provider=self.llm_compress_provider,
custom_token_counter=self.custom_token_counter,
Expand Down Expand Up @@ -166,7 +175,40 @@ async def reset(
# Light tool schema does not include tool parameters.
# This can reduce token usage when tools have large descriptions.
# See #4681
self.tool_schema_mode = tool_schema_mode
def _is_valid_prompt(prompt: str, placeholder: str) -> bool:
"""检查提示字符串是否有效:非空且包含恰好一个占位符"""
return prompt and prompt.count(placeholder) == 1

PLACEHOLDER = "{tool_names}"
DEFAULT_PROMPT = (
f"You have decided to call tool(s): {PLACEHOLDER}. "
f"Now call the tool(s) with required arguments using the tool schema, "
f"and follow the existing tool-use rules."
)
self.tool_call_requery_instruction_prompt = (
tool_call_requery_instruction_prompt
if _is_valid_prompt(tool_call_requery_instruction_prompt, PLACEHOLDER)
else DEFAULT_PROMPT
)
PLACEHOLDER = "{follow_up_lines}"
DEFAULT_PROMPT = (
"\n\n[SYSTEM NOTICE] User sent follow-up messages while tool execution "
"was in progress. Prioritize these follow-up instructions in your next "
"actions. In your very next action, briefly acknowledge to the user "
"that their follow-up message(s) were received before continuing.\n"
f"{PLACEHOLDER}"
)
self.tool_call_follow_up_notice_prompt = (
tool_call_follow_up_notice_prompt
if _is_valid_prompt(tool_call_follow_up_notice_prompt, PLACEHOLDER)
else DEFAULT_PROMPT
)
self.tool_call_max_step_reached_prompt = (
tool_call_max_step_reached_prompt
if tool_call_max_step_reached_prompt
else "工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。"
)

self._tool_schema_param_set = None
self._lazy_load_raw_tool_set = None
if tool_schema_mode == "lazy_load":
Expand Down Expand Up @@ -331,12 +373,8 @@ def _consume_follow_up_notice(self) -> str:
follow_up_lines = "\n".join(
f"{idx}. {ticket.text}" for idx, ticket in enumerate(follow_ups, start=1)
)
return (
"\n\n[SYSTEM NOTICE] User sent follow-up messages while tool execution "
"was in progress. Prioritize these follow-up instructions in your next "
"actions. In your very next action, briefly acknowledge to the user "
"that their follow-up message(s) were received before continuing.\n"
f"{follow_up_lines}"
return self.tool_call_follow_up_notice_prompt.format(
follow_up_lines=follow_up_lines
)

def _merge_follow_up_notice(self, content: str) -> str:
Expand Down Expand Up @@ -640,7 +678,7 @@ async def step_until_done(
self.run_context.messages.append(
Message(
role="user",
content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。",
content=self.tool_call_max_step_reached_prompt,
)
)
# 再执行最后一步
Expand Down Expand Up @@ -899,11 +937,8 @@ def _build_tool_requery_context(
contexts.append(msg.model_dump()) # type: ignore[call-arg]
elif isinstance(msg, dict):
contexts.append(copy.deepcopy(msg))
instruction = (
"You have decided to call tool(s): "
+ ", ".join(tool_names)
+ ". Now call the tool(s) with required arguments using the tool schema, "
"and follow the existing tool-use rules."
instruction = self.tool_call_requery_instruction_prompt.format(
tool_names=", ".join(tool_names)
)
if contexts and contexts[0].get("role") == "system":
content = contexts[0].get("content") or ""
Expand Down
83 changes: 72 additions & 11 deletions astrbot/core/astr_agent_tool_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ def _get_runtime_computer_tools(
cls,
runtime: str,
sandbox_cfg: dict | None = None,
local_cfg: dict | None = None,
session_id: str = "",
) -> dict[str, FunctionTool]:
from astrbot.core.computer.computer_tool_provider import ComputerToolProvider
Expand All @@ -240,6 +241,7 @@ def _get_runtime_computer_tools(
ctx = ToolProviderContext(
computer_use_runtime=runtime,
sandbox_cfg=sandbox_cfg,
local_cfg=local_cfg,
session_id=session_id,
)
tools = provider.get_tools(ctx)
Expand All @@ -264,9 +266,11 @@ def _build_handoff_toolset(
provider_settings = cfg.get("provider_settings", {})
runtime = str(provider_settings.get("computer_use_runtime", "local"))
sandbox_cfg = provider_settings.get("sandbox", {})
local_cfg = provider_settings.get("local", {})
runtime_computer_tools = cls._get_runtime_computer_tools(
runtime,
sandbox_cfg=sandbox_cfg,
local_cfg=local_cfg,
session_id=event.unified_msg_origin,
)

Expand Down Expand Up @@ -525,6 +529,8 @@ async def _wake_main_agent_for_background_result(

event = run_context.context.event
ctx = run_context.context.context
cfg = ctx.get_config(umo=event.unified_msg_origin)
proactive_cfg = cfg.get("provider_settings", {}).get("proactive_capability", {})

task_result = {
"task_id": task_id,
Expand Down Expand Up @@ -563,13 +569,54 @@ async def _wake_main_agent_for_background_result(
req.contexts = context
context_dump = req._print_friendly_context()
req.contexts = []
req.system_prompt += CONVERSATION_HISTORY_INJECT_PREFIX + context_dump
history_wrap_prompt = proactive_cfg.get(
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
"background_history_wrap_prompt",
CONVERSATION_HISTORY_INJECT_PREFIX
)
if history_wrap_prompt:
try:
req.system_prompt += history_wrap_prompt.format(
context_dump=context_dump
)
except Exception:
logger.error(
"background_history_wrap_prompt 格式化失败,回退到默认模板",
exc_info=True,
)
req.system_prompt += CONVERSATION_HISTORY_INJECT_PREFIX.format(
context_dump=context_dump
)
else:
req.system_prompt += CONVERSATION_HISTORY_INJECT_PREFIX.format(
context_dump=context_dump
)

bg = json.dumps(extras["background_task_result"], ensure_ascii=False)
req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
background_task_result=bg
background_execution_prompt = proactive_cfg.get(
"background_execution_prompt",
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT
)
if background_execution_prompt:
try:
req.system_prompt += background_execution_prompt.format(
background_task_result=bg
)
except Exception:
logger.error(
"background_execution_prompt 格式化失败,回退到默认模板",
exc_info=True,
)
req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
background_task_result=bg
)
else:
req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
background_task_result=bg
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 try-except 格式化文案逻辑在 history_wrap_promptbackground_execution_prompt 中是重复的。类似的逻辑也出现在 astrbot/core/cron/manager.py 中。

为了提高代码的可维护性并减少重复,建议将这个逻辑提取到一个辅助函数中。例如:

def _format_prompt_with_fallback(
    prompt_template: str | None,
    default_template: str,
    logger,
    log_message: str,
    **kwargs,
) -> str:
    """Helper to format a prompt with a fallback to a default template."""
    template_to_use = prompt_template or default_template
    try:
        return template_to_use.format(**kwargs)
    except Exception:
        logger.error(log_message, exc_info=True)
        return default_template.format(**kwargs)

# 使用示例:
# req.system_prompt += _format_prompt_with_fallback(
#     proactive_cfg.get("background_history_wrap_prompt"),
#     CONVERSATION_HISTORY_INJECT_PREFIX,
#     logger,
#     "background_history_wrap_prompt 格式化失败,回退到默认模板",
#     context_dump=context_dump,
# )

这样可以使代码更简洁,也更容易维护。

req.prompt = proactive_cfg.get(
"background_task_work_user_prompt",
BACKGROUND_TASK_WOKE_USER_PROMPT
)
req.prompt = BACKGROUND_TASK_WOKE_USER_PROMPT
if not req.func_tool:
req.func_tool = ToolSet()
req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)
Expand All @@ -587,15 +634,29 @@ async def _wake_main_agent_for_background_result(
pass
llm_resp = runner.get_final_llm_resp()
task_meta = extras.get("background_task_result", {})
summary_note = (
f"[BackgroundTask] {summary_name} "
f"(task_id={task_meta.get('task_id', task_id)}) finished. "
f"Result: {task_meta.get('result') or result_text or 'no content'}"
)

background_task_summary_note = proactive_cfg.get("background_task_summary_note", "")
try:
summary_note = background_task_summary_note.format(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Undefined result variable when formatting background_task_summary_note.

In _wake_main_agent_for_background_result, background_task_summary_note.format is called with result=result, but result isn’t defined in this scope, so this will always raise a NameError instead of producing the summary. This argument should likely come from task_meta.get('result'), result_text, or extras['background_task_result']['result'], to match the logic used in the fallback path.

summary_name=summary_name,
task_id=task_id,
result=result,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

这里存在一个 NameError bug。在 try 块中,background_task_summary_note.format() 使用了变量 result,但是 result 在这个作用域内尚未定义。它是在后面的 if llm_resp and llm_resp.completion_text: 块中才被定义的。

这会导致在 llm_resp 为空或没有 completion_text 时抛出 NameError 异常。

为了修复这个问题,你应该在 try 块之前定义 result。根据之前的代码逻辑,它应该是从 task_metaresult_text 中获取的。

        result_text = llm_resp.completion_text if llm_resp and llm_resp.completion_text else ""
        result_for_summary = task_meta.get('result') or result_text or 'no content'
        try:
            summary_note = background_task_summary_note.format(
                summary_name=summary_name,
                task_id=task_id,
                result=result_for_summary,
            )

except Exception:
summary_note = (
f"[BackgroundTask] {summary_name} "
f"(task_id={task_meta.get('task_id', task_id)}) finished. "
f"Result: {task_meta.get('result') or result_text or 'no content'}"
)
if llm_resp and llm_resp.completion_text:
summary_note += (
f"I finished the task, here is the result: {llm_resp.completion_text}"
background_task_summary_note_result = proactive_cfg.get(
"background_task_summary_note_result", ""
)
try:
result = background_task_summary_note_result.format(result=llm_resp.completion_text)
except Exception:
result = f"I finished the task, here is the result: {llm_resp.completion_text}"
summary_note += result
await persist_agent_history(
ctx.conversation_manager,
event=cron_event,
Expand Down
Loading