Files
zhenxun_bot/zhenxun/services/ai/flow/team/task_tools.py
T
922d092650 ♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)
* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统

- 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理
- 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册
- 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器
- 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签
- 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据
- 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度
- 重构执行分发器,支持并发限制、串行间隔和随机延迟打散
- 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入
- 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法
- 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出
- 将各模块的全局 logger 替换为对应的模块专属日志代理
- 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题
- 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token

* ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性

- 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构
- 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型
- 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义
- 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑
- 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构

* ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入

- 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性
- 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费
- 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-10 09:14:06 +08:00

195 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Annotated, Any
from pydantic import Field
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import EndRunResult, ToolResult
from zhenxun.services.ai.utils.logger import log_team as logger
from .models import TaskBoardState, TaskNodeStatus
class TaskPlanningToolkit(BaseToolkit):
"""
任务规划工具箱 (Planner Toolkit)。
大模型专用的黑板操作工具。大模型被剥夺了执行权,仅能拆解、指派和总结任务。
"""
default_prefix = ""
default_instructions = """<instructions>
## 🛠️ 任务规划工作流指南
你现在的角色是**项目经理 (Planner)**。你的唯一职责是拆解任务、分配人员并监控看板状态,**系统底层会自动拉起专家执行任务**。
1. **规划**:使用 `create_task` 拆解任务,设定 `assignee`(专家名称)和 `depends_on`(依赖的其它任务ID)。
2. **等待与监控**:每次你创建或更新任务后,请立刻停止工具调用,系统引擎会自动并发执行 pending 任务并再次唤醒你。
3. **🩹 智能自愈与重试**:如果你被唤醒后,看到看板上有任务处于 `failed` 状态,
请仔细阅读失败结果 (result)。你可以通过 `update_task_status` 将该任务的状态重新修改为 `pending`
以触发重新执行(可以附带修改建议在 result 里),或者创建新任务替代它。
4. **终结**:当你确认所有目标已达成时,调用 `mark_all_complete` 附上最终总结,正式结束整个流水线。
⚠️ 警告:你没有任何执行具体业务代码或查询的工具,你只能操作任务看板!
</instructions>""" # noqa: E501
def __init__(self, members: list[BaseRunnable], **kwargs):
super().__init__(**kwargs)
self.members = members
def _get_board(self, context: RunContext) -> TaskBoardState:
"""从运行上下文中安全的获取或初始化任务看板状态"""
if "__task_board__" not in context.session.shared_state:
context.session.shared_state["__task_board__"] = TaskBoardState()
return context.session.shared_state["__task_board__"]
@tool(description="创建一个新任务并加入看板。")
async def create_task(
self,
title: Annotated[str, Field(description="任务的简短、可行动的标题")],
description: Annotated[
str, Field(description="详细的任务说明,告诉执行者需要做什么以及期望的产出")
],
assignee: Annotated[
str,
Field(
description=(
"负责执行此任务的专家名称,必须完全匹配 <team_members> "
"中提供的 id,严禁捏造"
)
),
],
context: RunContext,
depends_on: Annotated[
list[str],
Field(
description=(
"该任务依赖的前置任务的【标题(title)】列表"
"(因同一回合创建时未知ID,请务必使用前置任务的 title 作为依赖)。"
"无依赖则必须传入空数组 []"
),
),
],
metadata: Annotated[
dict[str, Any],
Field(
description="可选的附加字典,用于向执行专家传递额外的结构化约束或参数"
),
] = {},
) -> ToolResult:
board = self._get_board(context)
valid_member_names = [m.name for m in self.members]
if assignee not in valid_member_names:
return ToolResult(
output=(
f"❌ 创建失败:未找到名为 '{assignee}' 的专家。"
f"可用专家: {valid_member_names}"
)
).as_error()
task = board.create_task(
title=title,
description=description,
assignee=assignee,
dependencies=depends_on,
metadata=metadata,
)
logger.debug(f" 🆕 [新建任务] `{task.title}` -> 👨💼{task.assignee}")
board_str = board.render_board_to_string()
return ToolResult(
output=(
f"✅ 任务创建成功!任务 ID: [{task.id}],"
f"状态: {task.status.value}\n\n{board_str}"
)
)
@tool(
description=(
"手动强制更新任务的状态(仅在特殊情况下使用,"
"因为 execute_task 会自动更新状态)。"
)
)
async def update_task_status(
self,
task_id: Annotated[str, Field(description="要更新的任务的唯一 ID")],
status: Annotated[
TaskNodeStatus,
Field(
description=(
"新的任务状态,支持: pending(用于重试), completed, failed 等"
)
),
],
context: RunContext,
result: Annotated[
str,
Field(
description=(
"提供结果、失败原因,或在设为 pending 重试时给执行专家的建议"
)
),
] = "",
) -> ToolResult:
board = self._get_board(context)
if status == TaskNodeStatus.in_progress:
return ToolResult(
output=(
"❌ 权限拒绝:你不能手动将任务状态设置为 in_progress。"
"该状态由底层执行引擎自动管理。如果你想让任务重新执行,"
"请将其设置为 pending。"
)
).as_error()
updated = board.update_task_status(task_id, status, result if result else None)
if not updated:
return ToolResult(output=f"❌ 找不到 ID 为 '{task_id}' 的任务。").as_error()
task_obj = board.get_task(task_id)
task_title = task_obj.title if task_obj else "Unknown"
logger.debug(f" 🔄 [任务状态变更] `{task_title}` -> {status.value}")
if task_obj and task_obj.status != status:
board_str = board.render_board_to_string()
return ToolResult(
output=(
"❌ 状态更新失败(触发底层状态机防呆回滚)!\n"
f"你尝试将 [{task_id}] 强制设置为 {status.value},"
f"但系统计算依赖图后将其重置为了 {task_obj.status.value}。\n"
"💡 原因分析:它的前置依赖(depends_on)可能尚未 COMPLETED,"
"或者你填错了依赖项的名称/ID导致系统无法追踪。\n\n"
f"{board_str}"
)
).as_error()
board_str = board.render_board_to_string()
return ToolResult(
output=f"✅ 任务 [{task_id}] 已更新为 {status.value}。\n\n{board_str}"
)
@tool(
description=(
"声明整体目标已完成。在调用此工具后,"
"大模型将被立刻中断并直接将 summary 返回给用户。"
)
)
async def mark_all_complete(
self,
summary: Annotated[
str,
Field(
description=(
"流程的最终战报。⚠️ 必须在总结中完整包含各专家产出的"
"核心交付物原文(如生成的故事、最终翻译内容等),绝对不能只说“已完成”!"
)
),
],
context: RunContext,
) -> ToolResult:
board = self._get_board(context)
board.is_goal_complete = True
board.final_summary = summary
return EndRunResult(output=summary)