Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
106 changes: 96 additions & 10 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,15 @@ async def reset(
self.stats = AgentStats()
self.stats.start_time = time.time()

self.max_step = 0 # 将在 step_until_done 中设置
self.current_step = 0

async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
"""Yields chunks *and* a final LLMResponse."""
messages = self._inject_todolist_if_needed(self.run_context.messages)

payload = {
"contexts": self.run_context.messages,
"contexts": messages,
"func_tool": self.req.func_tool,
"model": self.req.model, # NOTE: in fact, this arg is None in most cases
"session_id": self.req.session_id,
Expand All @@ -90,6 +95,87 @@ async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
else:
yield await self.provider.text_chat(**payload)

def _inject_todolist_if_needed(self, messages: list[Message]) -> list[Message]:
"""在Agent模式下注入TodoList和资源限制到消息列表"""
# 检查是否有 todolist 属性(更安全的方式,避免循环导入)
if not hasattr(self.run_context.context, "todolist"):
return messages

todolist = self.run_context.context.todolist
if not todolist:
return messages

# 构建注入内容
injection_parts = []

# 1. 资源限制部分
if hasattr(self, "max_step") and self.max_step > 0:
remaining = self.max_step - getattr(self, "current_step", 0)
current = getattr(self, "current_step", 0)
injection_parts.append(
f"--- 资源限制 ---\n"
f"剩余工具调用次数: {remaining}\n"
f"已调用次数: {current}\n"
f"请注意:请高效规划你的工作,尽量在工具调用次数用完之前完成任务。\n"
f"------------------"
)

# 2. TodoList部分
lines = ["--- 你当前的任务计划 ---"]
for task in todolist:
status_icon = {
"pending": "[ ]",
"in_progress": "[-]",
"completed": "[x]",
}.get(task.get("status", "pending"), "[ ]")
lines.append(f"{status_icon} #{task['id']}: {task['description']}")
lines.append("------------------------")
injection_parts.append("\n".join(lines))

# 合并所有注入内容
formatted_content = "\n\n".join(injection_parts)

# 使用智能注入,注入到 user 消息开头
return self._smart_inject_user_message(
messages, formatted_content, inject_at_start=True
)

def _smart_inject_user_message(
self,
messages: list[Message],
content_to_inject: str,
prefix: str = "",
inject_at_start: bool = False,
) -> list[Message]:
"""智能注入用户消息

Args:
messages: 消息列表
content_to_inject: 要注入的内容
prefix: 前缀文本(仅在新建消息时使用)
inject_at_start: 是否注入到 user 消息开头(默认注入到末尾)
"""
messages = list(messages)
if messages and messages[-1].role == "user":
last_msg = messages[-1]
if inject_at_start:
# 注入到 user 消息开头
messages[-1] = Message(
role="user", content=f"{content_to_inject}\n\n{last_msg.content}"
)
else:
# 注入到 user 消息末尾(默认行为)
messages[-1] = Message(
role="user",
content=f"{prefix}{content_to_inject}\n\n{last_msg.content}",
)
else:
# 添加新的 user 消息
messages.append(
Message(role="user", content=f"{prefix}{content_to_inject}")
)
return messages

@override
async def step(self):
"""Process a single step of the agent.
Expand Down Expand Up @@ -231,9 +317,11 @@ async def step_until_done(
self, max_step: int
) -> T.AsyncGenerator[AgentResponse, None]:
"""Process steps until the agent is done."""
step_count = 0
while not self.done() and step_count < max_step:
step_count += 1
self.max_step = max_step # 保存最大步数
self.current_step = 0

while not self.done() and self.current_step < max_step:
self.current_step += 1
async for resp in self.step():
yield resp

Expand All @@ -245,12 +333,10 @@ async def step_until_done(
# 拔掉所有工具
if self.req:
self.req.func_tool = None
# 注入提示词
self.run_context.messages.append(
Message(
role="user",
content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。",
)
# 智能注入提示词
self.run_context.messages = self._smart_inject_user_message(
self.run_context.messages,
"工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。",
)
# 再执行最后一步
async for resp in self.step():
Expand Down
11 changes: 11 additions & 0 deletions astrbot/core/agent/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Internal tools for Agent."""

from .todolist_tool import (
TODOLIST_ADD_TOOL,
TODOLIST_UPDATE_TOOL,
)

__all__ = [
"TODOLIST_ADD_TOOL",
"TODOLIST_UPDATE_TOOL",
]
102 changes: 102 additions & 0 deletions astrbot/core/agent/tools/todolist_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""TodoList Tool for Agent internal task management."""

from pydantic import Field
from pydantic.dataclasses import dataclass

from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext


@dataclass
class TodoListAddTool(FunctionTool[AstrAgentContext]):
name: str = "todolist_add"
description: str = (
"这个工具用于规划你的主要工作流程。请根据任务的整体复杂度,"
"添加3到7个主要的核心任务到待办事项列表中。每个任务应该是可执行的、明确的步骤。"
)
parameters: dict = Field(
default_factory=lambda: {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {"type": "string"},
"description": "List of task descriptions to add",
},
},
"required": ["tasks"],
}
)

async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> ToolExecResult:
tasks = kwargs.get("tasks", [])
if not tasks:
return "error: No tasks provided."

todolist = context.context.todolist
next_id = max([t["id"] for t in todolist], default=0) + 1

added = []
for desc in tasks:
task = {"id": next_id, "description": desc, "status": "pending"}
todolist.append(task)
added.append(f"#{next_id}: {desc}")
next_id += 1

return f"已添加 {len(added)} 个任务:\n" + "\n".join(added)


@dataclass
class TodoListUpdateTool(FunctionTool[AstrAgentContext]):
name: str = "todolist_update"
description: str = (
"Update a task's status or description in your todo list. "
"Status can be: pending, in_progress, completed."
)
parameters: dict = Field(
default_factory=lambda: {
"type": "object",
"properties": {
"task_id": {
"type": "integer",
"description": "ID of the task to update",
},
"status": {
"type": "string",
"description": "New status: pending, in_progress, or completed",
},
"description": {
"type": "string",
"description": "Optional new description",
},
},
"required": ["task_id", "status"],
}
)

async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> ToolExecResult:
# 检查必填参数
if "status" not in kwargs or kwargs.get("status") is None:
return "error: 参数缺失,status 是必填参数"

task_id = kwargs.get("task_id")
status = kwargs.get("status")
description = kwargs.get("description")

for task in context.context.todolist:
if task["id"] == task_id:
task["status"] = status
if description:
task["description"] = description
return f"已更新任务 #{task_id}: {task['description']} [{status}]"

return f"未找到任务 #{task_id}"


TODOLIST_ADD_TOOL = TodoListAddTool()
TODOLIST_UPDATE_TOOL = TodoListUpdateTool()
2 changes: 2 additions & 0 deletions astrbot/core/astr_agent_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class AstrAgentContext:
"""The message event associated with the agent context."""
extra: dict[str, str] = Field(default_factory=dict)
"""Customized extra data."""
todolist: list[dict] = Field(default_factory=list)
"""Agent's internal todo list for task management."""


AgentContextWrapper = ContextWrapper[AstrAgentContext]
7 changes: 7 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ class ChatProviderTemplate(TypedDict):
model: str
modalities: list
custom_extra_body: dict[str, Any]
max_context_length: int


CHAT_PROVIDER_TEMPLATE = {
Expand All @@ -187,6 +188,7 @@ class ChatProviderTemplate(TypedDict):
"model": "",
"modalities": [],
"custom_extra_body": {},
"max_context_length": 0, # 0 表示将从模型元数据自动填充
}

"""
Expand Down Expand Up @@ -1993,6 +1995,11 @@ class ChatProviderTemplate(TypedDict):
"type": "string",
"hint": "模型名称,如 gpt-4o-mini, deepseek-chat。",
},
"max_context_length": {
"description": "模型上下文窗口大小",
"type": "int",
"hint": "模型支持的最大上下文长度(Token数)。添加模型时会自动从模型元数据填充,也可以手动修改。留空或为0时将在保存时自动填充。",
},
"dify_api_key": {
"description": "API Key",
"type": "string",
Expand Down
22 changes: 22 additions & 0 deletions astrbot/core/context_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
AstrBot V2 上下文管理系统

统一的上下文压缩管理模块,实现多阶段处理流程:
1. Token初始统计 → 判断是否超过82%
2. 如果超过82%,执行压缩/截断(Agent模式/普通模式)
3. 最终处理:合并消息、清理Tool Calls、按数量截断
"""

from .context_compressor import ContextCompressor
from .context_manager import ContextManager
from .context_truncator import ContextTruncator
from .models import Message
from .token_counter import TokenCounter

__all__ = [
"ContextManager",
"TokenCounter",
"ContextTruncator",
"ContextCompressor",
"Message",
]
Loading