Record streaming summaries for agent tool calls

This commit is contained in:
jxxghp
2026-06-21 21:43:44 +08:00
parent 3c74f1bf58
commit 7358b4df14
7 changed files with 333 additions and 104 deletions

View File

@@ -1023,6 +1023,7 @@ class MoviePilotAgent:
skills_middleware = SkillsMiddleware(
sources=[str(agent_runtime_manager.skills_dir)],
bundled_skills_dir=str(settings.ROOT_PATH / "skills"),
stream_handler=self.stream_handler,
)
skill_tools = list(getattr(skills_middleware, "tools", []) or [])
activity_log_middleware = None
@@ -1030,6 +1031,7 @@ class MoviePilotAgent:
if self.has_message_context:
activity_log_middleware = ActivityLogMiddleware(
activity_dir=str(agent_runtime_manager.activity_dir),
stream_handler=self.stream_handler,
)
activity_log_tools = list(
getattr(activity_log_middleware, "tools", []) or []

View File

@@ -22,6 +22,7 @@ from langchain.agents.middleware.types import (
ModelResponse,
PrivateStateAttr, # noqa
ResponseT,
ToolCallRequest,
)
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langchain_core.tools import StructuredTool
@@ -495,10 +496,13 @@ class ActivityLogMiddleware(AgentMiddleware[ActivityLogState, ContextT, Response
activity_dir: str,
retention_days: int = DEFAULT_RETENTION_DAYS,
prompt_load_days: int = PROMPT_LOAD_DAYS,
stream_handler: Optional[Any] = None,
) -> None:
"""初始化活动日志中间件。"""
self.activity_dir = activity_dir
self.retention_days = retention_days
self.prompt_load_days = prompt_load_days
self.stream_handler = stream_handler
self._tool_provider = _ActivityLogToolProvider(activity_dir=activity_dir)
self.tools = [
StructuredTool.from_function(
@@ -646,6 +650,39 @@ class ActivityLogMiddleware(AgentMiddleware[ActivityLogState, ContextT, Response
modified_request = self.modify_request(request)
return await handler(modified_request)
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[Any]],
) -> Any:
"""在活动日志查询工具执行时记录聚合摘要。"""
tool = request.tool
tool_name = getattr(tool, "name", None)
if tool_name != QUERY_ACTIVITY_LOG_TOOL_NAME:
return await handler(request)
tool_call = request.tool_call or {}
tool_args = tool_call.get("args") or {}
if not isinstance(tool_args, dict):
tool_args = {}
logger.info(
f"开始执行活动日志查询工具: keyword={tool_args.get('keyword') or '-'}, "
f"date={tool_args.get('date') or '-'}"
)
if self.stream_handler and getattr(self.stream_handler, "is_streaming", False):
self.stream_handler.record_tool_call(
tool_name=QUERY_ACTIVITY_LOG_TOOL_NAME,
tool_message=QUERY_ACTIVITY_LOG_TOOL_DESCRIPTION,
tool_kwargs=tool_args,
)
try:
result = await handler(request)
except Exception as err:
logger.error(f"活动日志查询工具执行失败: error={err}")
raise
logger.info("活动日志查询工具执行完成")
return result
async def aafter_agent(
self, state: ActivityLogState, runtime: Runtime
) -> Optional[dict[str, Any]]:

View File

@@ -3,7 +3,7 @@ import re
import shutil
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import Annotated, List, Optional
from typing import Annotated, Any, List, Optional
from typing import NotRequired, TypedDict
import yaml # noqa
@@ -15,6 +15,7 @@ from langchain.agents.middleware.types import (
ModelRequest,
ModelResponse,
ResponseT,
ToolCallRequest,
)
from langchain.agents.middleware.types import PrivateStateAttr # noqa
from langchain_core.runnables import RunnableConfig
@@ -525,6 +526,7 @@ class SkillsMiddleware(AgentMiddleware[SkillsState, ContextT, ResponseT]): # no
*,
sources: list[str],
bundled_skills_dir: str | None = None,
stream_handler: Optional[Any] = None,
) -> None:
"""初始化 Skill 中间件。
@@ -535,9 +537,12 @@ class SkillsMiddleware(AgentMiddleware[SkillsState, ContextT, ResponseT]): # no
bundled_skills_dir : str | None
项目内置技能目录路径。若提供,在首次加载前会将其中不存在于
sources 首个目录的技能自动复制过去。
stream_handler : Optional[Any]
流式输出处理器,用于记录 skill 工具调用摘要。
"""
self.sources = sources
self.bundled_skills_dir = bundled_skills_dir
self.stream_handler = stream_handler
self.system_prompt_template = SKILLS_SYSTEM_PROMPT
self._skill_provider = _SkillToolProvider(sources=sources)
self.tools = [
@@ -584,7 +589,8 @@ class SkillsMiddleware(AgentMiddleware[SkillsState, ContextT, ResponseT]): # no
skills_catalog=_format_skill_tool_catalog(skills)
)
def _format_skills_list(self, skills: list[SkillMetadata]) -> str:
@staticmethod
def _format_skills_list(skills: list[SkillMetadata]) -> str:
"""格式化技能元数据列表用于系统提示词。"""
if not skills:
return "(No skills available yet.)"
@@ -657,5 +663,38 @@ class SkillsMiddleware(AgentMiddleware[SkillsState, ContextT, ResponseT]): # no
modified_request = self.modify_request(request)
return await handler(modified_request)
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[Any]],
) -> Any:
"""在 skill 工具执行时记录聚合摘要。"""
tool = request.tool
tool_name = getattr(tool, "name", None)
if tool_name != SKILL_TOOL_NAME:
return await handler(request)
tool_call = request.tool_call or {}
tool_args = tool_call.get("args") or {}
if not isinstance(tool_args, dict):
tool_args = {}
logger.info(
f"开始执行 Skill 工具: name={tool_args.get('name') or '-'}, "
f"explanation={tool_args.get('explanation') or '-'}"
)
if self.stream_handler and getattr(self.stream_handler, "is_streaming", False):
self.stream_handler.record_tool_call(
tool_name=SKILL_TOOL_NAME,
tool_message="Skill loaded",
tool_kwargs=tool_args,
)
try:
result = await handler(request)
except Exception as err:
logger.error(f"Skill 工具执行失败: error={err}")
raise
logger.info("Skill 工具执行完成")
return result
__all__ = ["SKILL_TOOL_NAME", "SkillMetadata", "SkillsMiddleware"]

View File

@@ -313,6 +313,31 @@ def _format_datetime(value: Optional[datetime]) -> Optional[str]:
return value.strftime("%Y-%m-%d %H:%M:%S")
def _extract_tool_call_args(request: ToolCallRequest) -> dict[str, Any]:
"""提取工具调用参数,并规整为字典。"""
tool_call = request.tool_call or {}
tool_args = tool_call.get("args") or {}
if not isinstance(tool_args, dict):
return {}
return tool_args
def _record_subagent_tool_call(
*,
stream_handler: Any,
tool_name: str,
tool_args: dict[str, Any],
) -> None:
"""在流式处理器中记录子代理工具调用摘要。"""
if not stream_handler or not getattr(stream_handler, "is_streaming", False):
return
stream_handler.record_tool_call(
tool_name=tool_name,
tool_message="Subagent invoked",
tool_kwargs=tool_args,
)
class _SubAgentAgentProvider:
"""子代理图懒加载与执行器。"""
@@ -409,8 +434,10 @@ class MoviePilotSubAgentMiddleware(AgentMiddleware):
tools: list[BaseTool],
system_prompt: str = SUBAGENT_PARENT_PROMPT,
task_description: str = SUBAGENT_TASK_DESCRIPTION,
stream_handler: Any = None,
) -> None:
self.system_prompt = system_prompt
self.stream_handler = stream_handler
self._provider = _SubAgentAgentProvider(
model=model,
profiles=profiles,
@@ -453,6 +480,35 @@ class MoviePilotSubAgentMiddleware(AgentMiddleware):
)
return await handler(request.override(system_message=new_system_message))
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[Any]],
) -> Any:
"""在 task 子代理工具执行时记录聚合摘要。"""
tool = request.tool
tool_name = getattr(tool, "name", None)
if tool_name != SUBAGENT_TASK_TOOL_NAME:
return await handler(request)
tool_args = _extract_tool_call_args(request)
logger.info(
f"开始执行子代理工具: tool_name={tool_name}, "
f"subagent_type={tool_args.get('subagent_type') or '-'}"
)
_record_subagent_tool_call(
stream_handler=self.stream_handler,
tool_name=SUBAGENT_TASK_TOOL_NAME,
tool_args=tool_args,
)
try:
result = await handler(request)
except Exception as err:
logger.error(f"子代理工具执行失败: tool_name={tool_name}, error={err}")
raise
logger.info(f"子代理工具执行完成: tool_name={tool_name}")
return result
class SubAgentTaskControlMiddleware(AgentMiddleware):
"""提供异步子代理任务调度工具的中间件。"""
@@ -464,8 +520,10 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
profiles: tuple[_SubAgentProfile, ...],
tools: list[BaseTool],
task_description: str = SUBAGENT_CONTROL_DESCRIPTION,
stream_handler: Any = None,
) -> None:
"""初始化异步子代理调度中间件。"""
self.stream_handler = stream_handler
self._provider = _SubAgentAgentProvider(
model=model,
profiles=profiles,
@@ -987,98 +1045,37 @@ class SubAgentTaskControlMiddleware(AgentMiddleware):
logger.info(f"Agent 结束,取消未完成子代理任务: tasks={len(unfinished_records)}")
await self._cancel_records(unfinished_records)
class SubAgentCallSummaryMiddleware(AgentMiddleware):
"""记录子代理调用次数的中间件。"""
def __init__(self, *, stream_handler: Any = None) -> None:
self.stream_handler = stream_handler
self.tools = []
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[Any]],
) -> Any:
"""在子代理任务工具执行时记录聚合摘要。"""
""" subagent_task 子代理工具执行时记录聚合摘要。"""
tool = request.tool
tool_name = getattr(tool, "name", None)
is_subagent_tool = tool_name in {
SUBAGENT_TASK_TOOL_NAME,
SUBAGENT_CONTROL_TOOL_NAME,
}
if is_subagent_tool:
tool_call = request.tool_call or {}
tool_args = tool_call.get("args") or {}
if not isinstance(tool_args, dict):
tool_args = {}
logger.info(
f"开始执行子代理工具: tool_name={tool_name}, "
f"action={tool_args.get('action') or '-'}, "
f"subagent_type={tool_args.get('subagent_type') or '-'}"
)
if (
self.stream_handler
and getattr(self.stream_handler, "is_streaming", False)
):
self.stream_handler.record_tool_call(
tool_name=tool_name or SUBAGENT_TASK_TOOL_NAME,
tool_message="Subagent invoked",
tool_kwargs=tool_args,
)
if tool_name != SUBAGENT_CONTROL_TOOL_NAME:
return await handler(request)
tool_args = _extract_tool_call_args(request)
logger.info(
f"开始执行子代理工具: tool_name={tool_name}, "
f"action={tool_args.get('action') or '-'}, "
f"subagent_type={tool_args.get('subagent_type') or '-'}"
)
_record_subagent_tool_call(
stream_handler=self.stream_handler,
tool_name=SUBAGENT_CONTROL_TOOL_NAME,
tool_args=tool_args,
)
try:
result = await handler(request)
except Exception as err:
if is_subagent_tool:
logger.error(f"子代理工具执行失败: tool_name={tool_name}, error={err}")
logger.error(f"子代理工具执行失败: tool_name={tool_name}, error={err}")
raise
if is_subagent_tool:
logger.info(f"子代理工具执行完成: tool_name={tool_name}")
logger.info(f"子代理工具执行完成: tool_name={tool_name}")
return result
def _deepagents_spec(
profiles: tuple[_SubAgentProfile, ...], tools: list[BaseTool]
) -> list[dict[str, Any]]:
"""将内置定义转换为 Deep Agents 子代理配置。"""
specs = []
for profile in profiles:
specs.append(
{
"name": profile.name,
"description": profile.description,
"prompt": profile.prompt,
"tools": _select_tools(tools, profile),
}
)
return specs
def _try_create_deepagents_middleware(
*,
profiles: tuple[_SubAgentProfile, ...],
tools: list[BaseTool],
model: BaseChatModel,
) -> Optional[AgentMiddleware]:
"""优先创建 Deep Agents 官方子代理中间件。"""
try:
from deepagents.backends import StateBackend
from deepagents.middleware.subagents import SubAgentMiddleware
return SubAgentMiddleware(
backend=StateBackend(),
subagents=_deepagents_spec(profiles, tools),
default_model=model,
system_prompt=SUBAGENT_PARENT_PROMPT,
task_description=SUBAGENT_TASK_DESCRIPTION,
)
except ImportError:
return None
except Exception as err:
logger.debug(f"Deep Agents 子代理中间件不可用,使用本地实现: {err}")
return None
def create_subagent_middlewares(
*,
model: BaseChatModel,
@@ -1089,21 +1086,17 @@ def create_subagent_middlewares(
_builtin_subagent_profiles.cache_clear()
builtin_subagent_names.cache_clear()
profiles = _builtin_subagent_profiles()
subagent_middleware = _try_create_deepagents_middleware(
subagent_middleware = MoviePilotSubAgentMiddleware(
model=model,
profiles=profiles,
tools=tools,
model=model,
stream_handler=stream_handler,
)
if subagent_middleware is None:
subagent_middleware = MoviePilotSubAgentMiddleware(
model=model,
profiles=profiles,
tools=tools,
)
control_middleware = SubAgentTaskControlMiddleware(
model=model,
profiles=profiles,
tools=tools,
stream_handler=stream_handler,
)
task_tools = [
@@ -1113,7 +1106,6 @@ def create_subagent_middlewares(
return [
subagent_middleware,
control_middleware,
SubAgentCallSummaryMiddleware(stream_handler=stream_handler),
], task_tools