2026-02-02 21:44:10 +08:00
|
|
|
|
"""附件注入中间件 - 使用 LangChain 标准中间件实现
|
|
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
从 State 中读取附件信息,注入提示词让模型使用 read_file 工具读取附件内容。
|
2026-02-02 21:44:10 +08:00
|
|
|
|
"""
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from collections.abc import Callable, Sequence
|
|
|
|
|
|
from typing import NotRequired
|
|
|
|
|
|
|
|
|
|
|
|
from langchain.agents import AgentState
|
|
|
|
|
|
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
|
2026-02-21 02:03:26 +08:00
|
|
|
|
from langchain_core.messages import SystemMessage
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
from src.utils import logger
|
|
|
|
|
|
|
2026-02-21 02:03:26 +08:00
|
|
|
|
ATTACHMENT_PROMPT_MARKER = "<!-- attachment_context -->"
|
|
|
|
|
|
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
class AttachmentState(AgentState):
|
|
|
|
|
|
"""扩展 AgentState 以支持附件"""
|
|
|
|
|
|
|
|
|
|
|
|
attachments: NotRequired[list[dict]]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
def _build_attachment_prompt(attachments: Sequence[dict]) -> str | None:
|
2026-02-02 21:44:10 +08:00
|
|
|
|
"""Render attachments into a system prompt block with file paths.
|
|
|
|
|
|
|
|
|
|
|
|
提示模型使用 read_file 工具读取附件内容。
|
|
|
|
|
|
"""
|
2025-11-08 10:51:30 +08:00
|
|
|
|
if not attachments:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
valid_attachments = [a for a in attachments if a.get("status") == "parsed"]
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2026-02-02 21:44:10 +08:00
|
|
|
|
if not valid_attachments:
|
|
|
|
|
|
return None
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2026-02-02 21:44:10 +08:00
|
|
|
|
attachment_infos: list[str] = []
|
2026-02-13 22:16:11 +08:00
|
|
|
|
for attachment in valid_attachments:
|
|
|
|
|
|
file_name = attachment.get("file_name", "未知文件")
|
|
|
|
|
|
file_path = attachment.get("file_path", "")
|
2025-11-08 10:51:30 +08:00
|
|
|
|
truncated = "(已截断)" if attachment.get("truncated") else ""
|
|
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
if file_path:
|
|
|
|
|
|
attachment_infos.append(f"- {file_name}{truncated}: {file_path}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
attachment_infos.append(f"- {file_name}{truncated}")
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2026-02-02 21:44:10 +08:00
|
|
|
|
lines = [
|
2026-02-13 22:16:11 +08:00
|
|
|
|
"用户上传了以下附件:",
|
2026-02-02 21:44:10 +08:00
|
|
|
|
"",
|
|
|
|
|
|
*attachment_infos,
|
|
|
|
|
|
"",
|
2026-02-13 22:16:11 +08:00
|
|
|
|
"请使用 read_file 工具读取附件内容后,再回答用户的问题。",
|
2026-02-02 21:44:10 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AttachmentMiddleware(AgentMiddleware[AttachmentState]):
|
|
|
|
|
|
"""
|
2026-02-13 22:16:11 +08:00
|
|
|
|
LangChain 标准中间件:从 State 中读取附件并注入提示词。
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
2026-02-13 22:16:11 +08:00
|
|
|
|
LangGraph 会自动从 checkpointer 恢复 state,包括 attachments。
|
2026-02-21 02:03:26 +08:00
|
|
|
|
从 request.state 中读取附件,将其转换为上下文块 并注入到系统提示词中。
|
2025-11-08 10:51:30 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
state_schema = AttachmentState
|
|
|
|
|
|
|
|
|
|
|
|
async def awrap_model_call(
|
|
|
|
|
|
self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
|
|
|
|
) -> ModelResponse:
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 从 state 获取附件(LangGraph 自动从 checkpointer 恢复)
|
2025-11-08 10:51:30 +08:00
|
|
|
|
attachments = request.state.get("attachments", [])
|
2026-02-03 02:28:51 +08:00
|
|
|
|
logger.info(f"AttachmentMiddleware: found {len(attachments)} attachments in state")
|
|
|
|
|
|
|
|
|
|
|
|
if attachments:
|
2026-02-13 22:16:11 +08:00
|
|
|
|
# 构建附件提示
|
|
|
|
|
|
attachment_prompt = _build_attachment_prompt(attachments)
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
if attachment_prompt:
|
2026-02-13 22:16:11 +08:00
|
|
|
|
logger.info("AttachmentMiddleware: injecting attachment prompt")
|
2026-02-21 02:03:26 +08:00
|
|
|
|
existing_blocks = list(request.system_message.content_blocks) if request.system_message else []
|
|
|
|
|
|
existing_text = "\n".join(
|
|
|
|
|
|
block.get("text", "")
|
|
|
|
|
|
for block in existing_blocks
|
|
|
|
|
|
if isinstance(block, dict) and block.get("type") == "text"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if ATTACHMENT_PROMPT_MARKER in existing_text:
|
|
|
|
|
|
logger.info("AttachmentMiddleware: attachment prompt already injected, skip")
|
|
|
|
|
|
return await handler(request)
|
|
|
|
|
|
|
|
|
|
|
|
merged_blocks = existing_blocks + [
|
|
|
|
|
|
{"type": "text", "text": f"{ATTACHMENT_PROMPT_MARKER}\n{attachment_prompt}"}
|
|
|
|
|
|
]
|
|
|
|
|
|
request = request.override(system_message=SystemMessage(content=merged_blocks))
|
2025-11-08 10:51:30 +08:00
|
|
|
|
|
|
|
|
|
|
return await handler(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 创建中间件实例,供其他模块使用
|
2026-02-02 21:44:10 +08:00
|
|
|
|
save_attachments_to_fs = AttachmentMiddleware()
|
|
|
|
|
|
|
|
|
|
|
|
# 保留旧名称以保持向后兼容(已废弃)
|
|
|
|
|
|
inject_attachment_context = save_attachments_to_fs
|