middleware.py 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """动态时间与 Skill 任务的工具提交收口;压缩和 HITL 由框架处理。"""
  2. from __future__ import annotations
  3. import re
  4. from typing import Any, Callable
  5. from langchain.agents.middleware.types import AgentMiddleware, ModelRequest, ModelResponse, hook_config
  6. from agents.prompts import build_time_block
  7. from obs import get_logger
  8. logger = get_logger(__name__)
  9. # 系统提示词里的时间段落。编译期写入的是占位内容,每次调用前整体替换。
  10. _TIME_BLOCK_RE = re.compile(r"\n## 系统时间\n.*?(?=\n## |\Z)", re.DOTALL)
  11. def _refresh_time(request: ModelRequest) -> ModelRequest:
  12. """把系统提示词里的「## 系统时间」段落刷新为当前时间。
  13. 没有系统消息、或 content 是结构化块(多模态)时原样返回:不去动它,
  14. 避免破坏块结构。
  15. """
  16. message = request.system_message
  17. if message is None:
  18. return request
  19. content = message.content
  20. if not isinstance(content, str):
  21. return request
  22. refreshed = _TIME_BLOCK_RE.sub(build_time_block(), content, count=1)
  23. if refreshed == content and "## 系统时间" not in content:
  24. refreshed = content + build_time_block()
  25. if refreshed == content:
  26. return request
  27. return request.override(system_message=message.model_copy(update={"content": refreshed}))
  28. class DynamicTimeMiddleware(AgentMiddleware):
  29. """每次模型调用前刷新系统提示词里的「## 系统时间」段落。
  30. 为什么需要:agent 是编译期单例、长驻进程。如果不刷新,系统提示词里的
  31. 时间会永久停在 agent 构建的那一刻——用户第二天问「今天几号」,模型会
  32. 拿到昨天的日期,且因为它「看得见」一个具体日期,比不知道更危险。
  33. **同步与异步两套钩子都要实现**:只实现 ``wrap_model_call`` 时,用
  34. ``astream``/``ainvoke`` 调用会在运行期抛 NotImplementedError——而且报错
  35. 发生在模型节点内部,离「你少写了个异步方法」这个原因很远。本服务全程
  36. 走异步路径,这里两个都实现,同步的实现顺带留给脚本与测试用。
  37. """
  38. def wrap_model_call(
  39. self,
  40. request: ModelRequest,
  41. handler: Callable[[ModelRequest], ModelResponse],
  42. ) -> Any:
  43. return handler(_refresh_time(request))
  44. async def awrap_model_call(
  45. self,
  46. request: ModelRequest,
  47. handler: Callable[[ModelRequest], Any],
  48. ) -> Any:
  49. return await handler(_refresh_time(request))
  50. class SkillCompletionMiddleware(AgentMiddleware):
  51. """提交工具写入正式结果后结束;校验失败仍交还模型修正。"""
  52. def before_agent(self, state, runtime):
  53. return {'structured_response': None}
  54. async def abefore_agent(self, state, runtime):
  55. return self.before_agent(state, runtime)
  56. def wrap_model_call(self, request, handler):
  57. # 完成通过提交工具表达,避免模型只写一段计划就结束队列任务。
  58. return handler(request.override(tool_choice='any'))
  59. async def awrap_model_call(self, request, handler):
  60. return await handler(request.override(tool_choice='any'))
  61. @hook_config(can_jump_to=['end'])
  62. def before_model(self, state, runtime):
  63. if state.get('structured_response') is not None:
  64. return {'jump_to': 'end'}
  65. return None
  66. @hook_config(can_jump_to=['end'])
  67. async def abefore_model(self, state, runtime):
  68. return self.before_model(state, runtime)
  69. __all__ = ["DynamicTimeMiddleware", "SkillCompletionMiddleware"]