sycommon-python-lib 0.2.8a1__py3-none-any.whl → 0.2.8a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sycommon/agent/deep_agent.py +180 -14
- sycommon/agent/middleware/model_request_prep.py +17 -2
- sycommon/agent/middleware/sensitive_guard.py +4 -0
- sycommon/agent/multi_agent_team.py +17 -5
- sycommon/agent/resume_compactor.py +223 -6
- sycommon/agent/summarization_utils.py +46 -34
- sycommon/logging/kafka_log.py +76 -2
- sycommon/middleware/tool_result_truncation.py +34 -6
- sycommon/middleware/traceid.py +30 -1
- sycommon/rabbitmq/rabbitmq_client.py +38 -2
- sycommon/rabbitmq/rabbitmq_pool.py +141 -63
- sycommon/rabbitmq/rabbitmq_service_core.py +72 -19
- sycommon/rabbitmq/rabbitmq_service_producer_manager.py +162 -80
- sycommon/synacos/nacos_config_manager.py +12 -0
- sycommon/tests/deep_agent_server.py +3 -2
- sycommon/tests/test_backend_factory_compat.py +158 -0
- sycommon/tests/test_deep_agent.py +5 -4
- sycommon/tests/test_nacos_llm_real.py +113 -0
- sycommon/tools/time_utils.py +75 -0
- {sycommon_python_lib-0.2.8a1.dist-info → sycommon_python_lib-0.2.8a3.dist-info}/METADATA +13 -13
- {sycommon_python_lib-0.2.8a1.dist-info → sycommon_python_lib-0.2.8a3.dist-info}/RECORD +24 -21
- {sycommon_python_lib-0.2.8a1.dist-info → sycommon_python_lib-0.2.8a3.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.2.8a1.dist-info → sycommon_python_lib-0.2.8a3.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.8a1.dist-info → sycommon_python_lib-0.2.8a3.dist-info}/top_level.txt +0 -0
sycommon/agent/deep_agent.py
CHANGED
|
@@ -76,16 +76,67 @@ _STREAM_DEBUG = check_env_flag(['DEEP_AGENT_STREAM_DEBUG'], default='false')
|
|
|
76
76
|
# 无上限可涨到几十 MB。超限停止追加(只影响前端 tool_call 进度展示,工具实际执行不受影响)。
|
|
77
77
|
MAX_ARGS_BUFFER_BYTES = int(os.environ.get("DEEP_AGENT_MAX_ARGS_BUFFER", 1024 * 1024))
|
|
78
78
|
|
|
79
|
+
# Defense 4 流式缓冲的判定阈值(字符):累积到该长度做一次备忘复述前缀判定。
|
|
80
|
+
# 需 >= _match_summary_prefix_overlap 的 min_overlap(20) 且覆盖典型复述开头。
|
|
81
|
+
_DEFENSE4_PROBE_CHARS = int(os.environ.get("DEEP_AGENT_DEFENSE4_PROBE", 120))
|
|
82
|
+
|
|
79
83
|
if TYPE_CHECKING:
|
|
80
84
|
from deepagents.graph import CompiledStateGraph
|
|
81
85
|
from langchain.agents.middleware.types import AgentState, ContextT, ModelRequest, ModelResponse, ResponseT
|
|
82
86
|
from langgraph.runtime import Runtime
|
|
83
87
|
|
|
88
|
+
|
|
89
|
+
def _match_summary_prefix_overlap(text: str, summary: str,
|
|
90
|
+
min_overlap: int = 20) -> int:
|
|
91
|
+
"""检测回答开头对备忘原文的逐字复述,返回需剥除的字符数(0=无复述)。
|
|
92
|
+
|
|
93
|
+
Defense 4 的核心判定:模型复述备忘时几乎总是从备忘某行的开头逐字照抄
|
|
94
|
+
(实测样本:「用户的整体目标是深入学习...」= 备忘第一句原文)。这里对
|
|
95
|
+
备忘的每一条目(按行取正文)做前缀匹配:text 开头是某条目的开头片段
|
|
96
|
+
(且 >= min_overlap 字符)即命中,返回重叠长度。
|
|
97
|
+
|
|
98
|
+
只查开头(前缀),不做全文搜索 —— 备忘要素"融入回答"是期望行为,
|
|
99
|
+
只有开头照抄才是复述泄漏。正常回答恰好以备忘某条目前 20 字开头且
|
|
100
|
+
语义独立的概率可忽略;误剥时也只影响首个 chunk 的前缀。
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
text: AI 回答的首个非空文本 chunk。
|
|
104
|
+
summary: 注入 system 的备忘原文(电报式条目,逐行比对)。
|
|
105
|
+
min_overlap: 判定为复述的最小连续重叠字符数。
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
text 开头与备忘条目重叠的字符数;无重叠返回 0。
|
|
109
|
+
"""
|
|
110
|
+
if not text or not summary:
|
|
111
|
+
return 0
|
|
112
|
+
probe = text[:120] # 复述只发生在开头,截断降低比对开销
|
|
113
|
+
for line in summary.splitlines():
|
|
114
|
+
line = line.strip()
|
|
115
|
+
if len(line) < min_overlap:
|
|
116
|
+
continue
|
|
117
|
+
# 条目锚点:行首(条目化后「目标:」等标签开头)+ 去掉标签后的正文开头
|
|
118
|
+
for anchor in (line, line.split(":", 1)[-1].lstrip()):
|
|
119
|
+
anchor = anchor.strip()
|
|
120
|
+
if len(anchor) < min_overlap:
|
|
121
|
+
continue
|
|
122
|
+
# 前缀重叠:probe 与 anchor 的最长公共前缀
|
|
123
|
+
n = min(len(probe), len(anchor))
|
|
124
|
+
common = 0
|
|
125
|
+
for i in range(n):
|
|
126
|
+
if probe[i] == anchor[i]:
|
|
127
|
+
common += 1
|
|
128
|
+
else:
|
|
129
|
+
break
|
|
130
|
+
if common >= min_overlap:
|
|
131
|
+
return common
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
84
135
|
@tool
|
|
85
136
|
def get_current_date() -> str:
|
|
86
|
-
"""
|
|
87
|
-
from
|
|
88
|
-
return
|
|
137
|
+
"""获取当前日期时间(北京时间)"""
|
|
138
|
+
from sycommon.tools.time_utils import now_bj
|
|
139
|
+
return now_bj().strftime("%Y-%m-%d %H:%M:%S")
|
|
89
140
|
|
|
90
141
|
|
|
91
142
|
# 默认系统提示词
|
|
@@ -232,6 +283,8 @@ class DeepAgent:
|
|
|
232
283
|
rubric_eval_collector: Optional[_RubricEvalCollector] = None,
|
|
233
284
|
sensitive_guard: Optional[SensitiveGuardMiddleware] = None,
|
|
234
285
|
raw_model: Any = None,
|
|
286
|
+
request_prep: Optional[ModelRequestPrepMiddleware] = None,
|
|
287
|
+
summarization_mw: Any = None,
|
|
235
288
|
):
|
|
236
289
|
self.user_id = user_id
|
|
237
290
|
self.agent = agent
|
|
@@ -240,6 +293,12 @@ class DeepAgent:
|
|
|
240
293
|
self.recovery_manager = recovery_manager
|
|
241
294
|
self.rubric_eval_collector = rubric_eval_collector
|
|
242
295
|
self.sensitive_guard = sensitive_guard
|
|
296
|
+
# 🔑 ModelRequestPrepMiddleware 实例引用:chat() 时读取其
|
|
297
|
+
# last_summary_text 做备忘复述兜底(Defense 4)。
|
|
298
|
+
self.request_prep = request_prep
|
|
299
|
+
# 🔑 SummarizationToolMiddleware 内部 middleware 引用:resume_compactor
|
|
300
|
+
# 恢复前分段压缩复用其 _acreate_summary/_partition_messages 等压缩链。
|
|
301
|
+
self.summarization_mw = summarization_mw
|
|
243
302
|
# 🔑 存 raw_model 引用,供 cleanup 关 LLM client(root_async_client)。
|
|
244
303
|
# model 被埋进 graph 内部节点闭包,表层拿不到,必须 create 时显式传入。
|
|
245
304
|
self._raw_model = raw_model
|
|
@@ -290,6 +349,13 @@ class DeepAgent:
|
|
|
290
349
|
ai_text_content = ""
|
|
291
350
|
seen_tool_call_ids = set()
|
|
292
351
|
stream_step = 0
|
|
352
|
+
# Defense 4(备忘复述兜底)状态:对每「版」备忘各判定一次。
|
|
353
|
+
# 比对文本取 ModelRequestPrepMiddleware.last_summary_text —— 压缩可发生
|
|
354
|
+
# 在本轮任意一次模型调用(不只在首轮)。流式下逐 token chunk 太小,
|
|
355
|
+
# 用缓冲累积到 ~120 字符再做一次判定(见流循环内 Defense 4 注释)。
|
|
356
|
+
_defense4_seen_summary = None
|
|
357
|
+
_defense4_buffer = ""
|
|
358
|
+
_defense4_done = True # 无备忘时默认已完成(跳过判定)
|
|
293
359
|
# 兜底:累积流式 chunk 中的 usage_metadata(middleware 在流式场景可能拿不到)
|
|
294
360
|
total_input_tokens = 0
|
|
295
361
|
total_output_tokens = 0
|
|
@@ -405,6 +471,14 @@ class DeepAgent:
|
|
|
405
471
|
|
|
406
472
|
stream_step += 1
|
|
407
473
|
|
|
474
|
+
# 🔑 跳过 middleware 内部模型调用的输出(lc_internal_call)。
|
|
475
|
+
# langchain 1.3.15 起 SummarizationMiddleware 的摘要生成调用带
|
|
476
|
+
# internal_call_metadata() 标记(进程内随机 token 防伪造),
|
|
477
|
+
# langgraph 会把它透传到流事件 metadata。压缩轮的「摘要本体」
|
|
478
|
+
# (目标:/事实: 等条目)曾因此被当回答发给用户 —— 根治点。
|
|
479
|
+
if isinstance(metadata, dict) and "lc_internal_call" in metadata:
|
|
480
|
+
continue
|
|
481
|
+
|
|
408
482
|
# ===== 逐 chunk 诊断日志:默认关闭,DEEP_AGENT_STREAM_DEBUG=true 时开启 =====
|
|
409
483
|
if _STREAM_DEBUG:
|
|
410
484
|
_chunk_type = type(chunk).__name__
|
|
@@ -569,6 +643,46 @@ class DeepAgent:
|
|
|
569
643
|
if _cleaned:
|
|
570
644
|
content = content.strip()
|
|
571
645
|
|
|
646
|
+
# 🔑 Defense 4(输出兜底):开头与注入备忘的复述检测。
|
|
647
|
+
# 前三层防线(备忘条目化 / 正向契约 / 标签清理)失效时,模型会把
|
|
648
|
+
# 备忘开头当回答开头逐字复述(实测"用户的整体目标是...")。这里
|
|
649
|
+
# 对本条 agent 回答的开头做一次重叠判定:开头与备忘原文出现连续
|
|
650
|
+
# 长重叠(>= 20 字符)即剥掉重叠前缀。
|
|
651
|
+
# 🔑 流式适配:逐 token 流式下首 chunk 仅 1-2 字符,单个 chunk
|
|
652
|
+
# 永远达不到 20 字符判定阈值。因此用「先缓冲后放行」:本条回答的
|
|
653
|
+
# 前 _DEFENSE4_PROBE_CHARS 字符先累积到 _defense4_buffer(不
|
|
654
|
+
# yield),凑满后做一次判定——命中复述剥除前缀、未命中原样,
|
|
655
|
+
# 然后一次性 flush 发出;此后所有 chunk 直通。回答总长不足阈值
|
|
656
|
+
# 时由流末 flush(见 FOR 循环结束处)兜底。代价是回答开头延迟
|
|
657
|
+
# 若干 token 到达,换取首句复述的可靠拦截。
|
|
658
|
+
if (msg_type in ("AIMessage", "AIMessageChunk")
|
|
659
|
+
and content.strip()):
|
|
660
|
+
_summary_ref = getattr(
|
|
661
|
+
getattr(self, 'request_prep', None),
|
|
662
|
+
'last_summary_text', None)
|
|
663
|
+
# 新一版备忘 → 重置缓冲与完成标记(压缩可发生在本轮任意一次模型调用后)
|
|
664
|
+
if _summary_ref and _summary_ref is not _defense4_seen_summary:
|
|
665
|
+
_defense4_seen_summary = _summary_ref
|
|
666
|
+
_defense4_buffer = ""
|
|
667
|
+
_defense4_done = False
|
|
668
|
+
if _summary_ref and not _defense4_done:
|
|
669
|
+
_defense4_buffer += content
|
|
670
|
+
if len(_defense4_buffer) >= _DEFENSE4_PROBE_CHARS:
|
|
671
|
+
_defense4_done = True
|
|
672
|
+
_strip = _match_summary_prefix_overlap(
|
|
673
|
+
_defense4_buffer, _summary_ref)
|
|
674
|
+
if _strip:
|
|
675
|
+
SYLogger.warning(
|
|
676
|
+
f"[DeepAgent] 回答开头复述备忘(重叠 {_strip} 字符),已剥除")
|
|
677
|
+
_defense4_buffer = _defense4_buffer[_strip:].lstrip()
|
|
678
|
+
# 缓冲未满或刚好判定完:本 chunk 已并入缓冲,不单独发出。
|
|
679
|
+
# 判定完成时下方统一 flush;未满时置空等待后续。
|
|
680
|
+
content = ""
|
|
681
|
+
# 判定完成后:缓冲里剩余的内容一次性放到本 chunk 发出
|
|
682
|
+
if _defense4_done and _defense4_buffer:
|
|
683
|
+
content = _defense4_buffer + (content or "")
|
|
684
|
+
_defense4_buffer = ""
|
|
685
|
+
|
|
572
686
|
if msg_type in ("AIMessage", "AIMessageChunk"):
|
|
573
687
|
# 🔑 提取 reasoning_content(思考/推理内容)
|
|
574
688
|
# 模型在 thinking 模式下会将推理过程放在 reasoning_content 字段,
|
|
@@ -781,6 +895,28 @@ class DeepAgent:
|
|
|
781
895
|
print(
|
|
782
896
|
f"[DeepAgent] FOR LOOP ENDED. stream_step={stream_step}, ai_text_content={repr(ai_text_content[:100])}, tool_calls={len(current_tool_calls)}", flush=True)
|
|
783
897
|
|
|
898
|
+
# 🔑 Defense 4 流末 flush:回答总长不足判定阈值时缓冲里还压着内容
|
|
899
|
+
# (正常路径在缓冲满 _DEFENSE4_PROBE_CHARS 时已 flush)。这里做
|
|
900
|
+
# 最后一次判定并发出,避免短回答被缓冲吞掉。
|
|
901
|
+
if _defense4_buffer:
|
|
902
|
+
if not _defense4_done and _defense4_seen_summary:
|
|
903
|
+
_strip = _match_summary_prefix_overlap(
|
|
904
|
+
_defense4_buffer, _defense4_seen_summary)
|
|
905
|
+
if _strip:
|
|
906
|
+
SYLogger.warning(
|
|
907
|
+
f"[DeepAgent] 回答开头复述备忘(流末判定,重叠 {_strip} 字符),已剥除")
|
|
908
|
+
_defense4_buffer = _defense4_buffer[_strip:].lstrip()
|
|
909
|
+
if _defense4_buffer:
|
|
910
|
+
flushed = _defense4_buffer
|
|
911
|
+
_defense4_buffer = ""
|
|
912
|
+
ai_text_content += flushed
|
|
913
|
+
event = ChatEventBuilder.ai_chunk(
|
|
914
|
+
flushed, id=None, agent=DEFAULT_AGENT_NAME)
|
|
915
|
+
if on_event:
|
|
916
|
+
await on_event(event)
|
|
917
|
+
yield event
|
|
918
|
+
_defense4_done = True
|
|
919
|
+
|
|
784
920
|
SYLogger.warning(
|
|
785
921
|
f"[STREAM-DEBUG] FOR LOOP ENDED | _astream_chunk_count={_astream_chunk_count} | "
|
|
786
922
|
f"stream_step={stream_step} | ai_text_len={len(ai_text_content)} | "
|
|
@@ -1115,7 +1251,8 @@ async def create_deep_agent(
|
|
|
1115
1251
|
|
|
1116
1252
|
middleware_list = [
|
|
1117
1253
|
guard,
|
|
1118
|
-
|
|
1254
|
+
# 截断上限从 nacos maxTokens 推导(×0.04,下限2000);读不到配置兜底 8192
|
|
1255
|
+
ToolResultTruncationMiddleware(model_name=config.model_name),
|
|
1119
1256
|
TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
|
|
1120
1257
|
summarization_mw,
|
|
1121
1258
|
ModelRequestPrepMiddleware(), # 紧跟 summarization:摘要重定位 + 动态压 max_tokens
|
|
@@ -1211,6 +1348,15 @@ async def create_deep_agent(
|
|
|
1211
1348
|
rubric_eval_collector=rubric_eval_collector,
|
|
1212
1349
|
sensitive_guard=guard,
|
|
1213
1350
|
raw_model=raw_model,
|
|
1351
|
+
request_prep=next(
|
|
1352
|
+
(m for m in middleware_list
|
|
1353
|
+
if type(m).__name__ == "ModelRequestPrepMiddleware"), None),
|
|
1354
|
+
# SummarizationToolMiddleware 实例引用:resume_compactor 分段压缩复用
|
|
1355
|
+
# 其内部 _DeepAgentsSummarizationMiddleware(_acreate_summary 等)。
|
|
1356
|
+
# graph 内部探测(deepagents 未暴露 middleware 列表)不可靠,创建时显式传入。
|
|
1357
|
+
summarization_mw=next(
|
|
1358
|
+
(getattr(m, "_summarization", None) for m in middleware_list
|
|
1359
|
+
if type(m).__name__ == "SummarizationToolMiddleware"), None),
|
|
1214
1360
|
)
|
|
1215
1361
|
|
|
1216
1362
|
|
|
@@ -1436,6 +1582,8 @@ async def _sync_skills_to_sandbox(
|
|
|
1436
1582
|
|
|
1437
1583
|
skill_count = 0
|
|
1438
1584
|
skip_count = 0
|
|
1585
|
+
# 收集需要重传的技能(版本不一致 / 无版本 / 正文校验失败)
|
|
1586
|
+
pending = [] # [(skill_name, local_version, src, remote_version)]
|
|
1439
1587
|
for skill_name, local_version, src in local_skills:
|
|
1440
1588
|
remote_version = remote_version_map.get(skill_name, "")
|
|
1441
1589
|
|
|
@@ -1444,19 +1592,27 @@ async def _sync_skills_to_sandbox(
|
|
|
1444
1592
|
# (历史上出现过沙箱 SKILL.md 版本号新、正文旧的脏数据,导致 agent
|
|
1445
1593
|
# 永远读不到新版指令)。校验失败则强制走删除+重传。
|
|
1446
1594
|
# fresh_map 在进入循环前已并发预取,这里只查表不再 await。
|
|
1447
|
-
if
|
|
1448
|
-
SYLogger.warning(
|
|
1449
|
-
f"[DeepAgent] 技能版本一致但正文不一致,强制重传: {skill_name} "
|
|
1450
|
-
f"v{local_version}")
|
|
1451
|
-
# 落到下方的删除+重传分支
|
|
1452
|
-
else:
|
|
1595
|
+
if fresh_map.get(skill_name, False):
|
|
1453
1596
|
SYLogger.debug(
|
|
1454
1597
|
f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
|
|
1455
1598
|
backend._synced_skills.add(skill_name)
|
|
1456
1599
|
skip_count += 1
|
|
1457
1600
|
continue
|
|
1458
|
-
|
|
1459
|
-
|
|
1601
|
+
SYLogger.warning(
|
|
1602
|
+
f"[DeepAgent] 技能版本一致但正文不一致,强制重传: {skill_name} "
|
|
1603
|
+
f"v{local_version}")
|
|
1604
|
+
pending.append((skill_name, local_version, src, remote_version))
|
|
1605
|
+
|
|
1606
|
+
# 并发删除 + 全量上传:信号量限流,把「N 次串行 HTTP 上传」压成「N/并发 批」。
|
|
1607
|
+
# 单个技能上传失败仅记 warning、不拖垮其余(原串行版会让一个失败中断后续所有技能)。
|
|
1608
|
+
# 内存安全:技能库总量 ~2.9MB / 最大单文件 ~608KB,base64 编码瞬时膨胀 ~3x,
|
|
1609
|
+
# 6 路并发最坏额外内存峰值 ~10MB 量级,对服务进程可忽略;若未来技能塞入大资源文件,
|
|
1610
|
+
# 可调低此值或改走 aupload_stream_from_disk 流式上传。
|
|
1611
|
+
_SKILL_SYNC_CONCURRENCY = 6
|
|
1612
|
+
_sync_sem = asyncio.Semaphore(_SKILL_SYNC_CONCURRENCY)
|
|
1613
|
+
|
|
1614
|
+
async def _sync_one(skill_name, local_version, src, remote_version):
|
|
1615
|
+
# 删除旧目录(走 /delete,跳过懒同步,毫秒级)
|
|
1460
1616
|
if skill_name in existing_dirs:
|
|
1461
1617
|
deleted_ok = False
|
|
1462
1618
|
for _attempt in range(2): # 最多重试一次,确保旧内容真删掉
|
|
@@ -1480,11 +1636,21 @@ async def _sync_skills_to_sandbox(
|
|
|
1480
1636
|
f"[DeepAgent] 删除旧版技能彻底失败,本次跳过: {skill_name}")
|
|
1481
1637
|
|
|
1482
1638
|
# 全量上传整个技能目录到沙箱(batch_upload 服务端无条件覆盖)
|
|
1483
|
-
|
|
1639
|
+
try:
|
|
1640
|
+
async with _sync_sem:
|
|
1641
|
+
await backend.async_sync_dirs([(src, f"{sandbox_dest}/{skill_name}")])
|
|
1642
|
+
except Exception as e:
|
|
1643
|
+
SYLogger.warning(
|
|
1644
|
+
f"[DeepAgent] 技能上传失败: {skill_name}, {e}")
|
|
1645
|
+
return 0
|
|
1484
1646
|
backend._synced_skills.add(skill_name)
|
|
1485
|
-
skill_count += 1
|
|
1486
1647
|
SYLogger.info(
|
|
1487
1648
|
f"[DeepAgent] 全量同步技能到沙箱: {skill_name} v{local_version}")
|
|
1649
|
+
return 1
|
|
1650
|
+
|
|
1651
|
+
if pending:
|
|
1652
|
+
results = await asyncio.gather(*[_sync_one(*p) for p in pending])
|
|
1653
|
+
skill_count = sum(r for r in results if r)
|
|
1488
1654
|
|
|
1489
1655
|
if chmod_readonly:
|
|
1490
1656
|
try:
|
|
@@ -45,8 +45,16 @@ class ModelRequestPrepMiddleware(AgentMiddleware):
|
|
|
45
45
|
通过 ``awrap_model_call`` 在调用 handler 前改造 request。与
|
|
46
46
|
``TokenTrackingMiddleware`` / ``SensitiveGuardMiddleware`` 同一套写法,
|
|
47
47
|
需排在 ``SummarizationMiddleware`` 之后(摘要 HumanMessage 由后者产生)。
|
|
48
|
+
|
|
49
|
+
另持有 ``last_summary_text``(最近一次注入 system 的备忘原文),
|
|
50
|
+
供 deep_agent 的输出兜底(首句与备忘重叠检测)使用。同一 agent 实例
|
|
51
|
+
串行会话下读取方在流式消费阶段访问,与写入(模型调用前)天然错开,
|
|
52
|
+
无并发竞争;多轮压缩时被新摘要覆盖,兜底始终比对最新一版。
|
|
48
53
|
"""
|
|
49
54
|
|
|
55
|
+
#: 最近一次注入 system_message 的备忘原文(无摘要时为 None)
|
|
56
|
+
last_summary_text: Optional[str] = None
|
|
57
|
+
|
|
50
58
|
@property
|
|
51
59
|
def name(self) -> str:
|
|
52
60
|
return "ModelRequestPrepMiddleware"
|
|
@@ -77,13 +85,20 @@ class ModelRequestPrepMiddleware(AgentMiddleware):
|
|
|
77
85
|
return request
|
|
78
86
|
|
|
79
87
|
from deepagents.middleware._utils import append_to_system_message
|
|
88
|
+
# 🔑 注入形态 = 备忘在前、行为契约在后(recency:靠近对话末端的指令
|
|
89
|
+
# 遵循度最高),且用正向指令替代"不要复述"禁令 —— 负向禁令对
|
|
90
|
+
# glm/DeepSeek 系模型有反向唤起效果(提到"复述"反而引导复述)。
|
|
80
91
|
_summary_directive = (
|
|
81
92
|
"\n\n<system_context>\n"
|
|
82
|
-
"
|
|
83
|
-
"把它当作你自己的回忆,绝对不要在回复中重复、复述或引用此内容的任何部分。\n"
|
|
93
|
+
"以下是之前对话历史的压缩备忘(内部笔记,仅供你参考,不属于对话内容)。\n"
|
|
84
94
|
f"{_summary_text}\n"
|
|
95
|
+
"\n回复规范:直接以对新问题的回答开头。"
|
|
96
|
+
"备忘中的相关要素直接融入回答本身;"
|
|
97
|
+
"不要先盘点、复述或引用备忘内容,也不要提及备忘的存在。\n"
|
|
85
98
|
"</system_context>"
|
|
86
99
|
)
|
|
100
|
+
# 记录备忘原文,供 deep_agent 输出兜底(首句重叠检测)比对
|
|
101
|
+
self.last_summary_text = _summary_text
|
|
87
102
|
_new_sys = append_to_system_message(
|
|
88
103
|
request.system_message, _summary_directive)
|
|
89
104
|
return request.override(messages=_filtered, system_message=_new_sys)
|
|
@@ -14,6 +14,7 @@ agent 本就是内部模型(model_tier="internal")时短路放行,不检
|
|
|
14
14
|
"""
|
|
15
15
|
import asyncio
|
|
16
16
|
import os
|
|
17
|
+
import time
|
|
17
18
|
from typing import Any, Awaitable, Callable, List, Optional
|
|
18
19
|
|
|
19
20
|
from langchain.agents.middleware.types import (
|
|
@@ -458,10 +459,13 @@ class SensitiveGuardMiddleware(AgentMiddleware):
|
|
|
458
459
|
# 恢复,让检测器在干净的上下文里跑(非流式、不进用户事件流)。
|
|
459
460
|
from langchain_core.runnables.config import var_child_runnable_config
|
|
460
461
|
_ctx_token = var_child_runnable_config.set({})
|
|
462
|
+
_t0 = time.perf_counter()
|
|
461
463
|
try:
|
|
462
464
|
raw = await detector.ainvoke(prompt)
|
|
463
465
|
finally:
|
|
464
466
|
var_child_runnable_config.reset(_ctx_token)
|
|
467
|
+
_dt = time.perf_counter() - _t0
|
|
468
|
+
SYLogger.info(f"[Perf] sensitive_detect: {_dt:.3f}s")
|
|
465
469
|
text = raw.content if isinstance(raw, BaseMessage) else str(raw)
|
|
466
470
|
check = _parse_detect_json(text)
|
|
467
471
|
if check is None:
|
|
@@ -60,9 +60,9 @@ if TYPE_CHECKING:
|
|
|
60
60
|
|
|
61
61
|
@tool
|
|
62
62
|
def get_current_date() -> str:
|
|
63
|
-
"""
|
|
64
|
-
from
|
|
65
|
-
return
|
|
63
|
+
"""获取当前日期时间(北京时间)"""
|
|
64
|
+
from sycommon.tools.time_utils import now_bj
|
|
65
|
+
return now_bj().strftime("%Y-%m-%d %H:%M:%S")
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
# 默认记忆系统提示词(所有角色共享)
|
|
@@ -208,6 +208,7 @@ class MultiAgentTeam:
|
|
|
208
208
|
downgrade_state: Optional[DowngradeState] = None,
|
|
209
209
|
sensitive_guard: Optional["SensitiveGuardMiddleware"] = None,
|
|
210
210
|
raw_model: Any = None,
|
|
211
|
+
summarization_mw: Any = None,
|
|
211
212
|
):
|
|
212
213
|
self.user_id = user_id
|
|
213
214
|
self.agent = agent
|
|
@@ -224,6 +225,9 @@ class MultiAgentTeam:
|
|
|
224
225
|
self.sensitive_guard = sensitive_guard
|
|
225
226
|
# 🔑 存协调者 raw_model 引用,供 cleanup 关 LLM client
|
|
226
227
|
self._raw_model = raw_model
|
|
228
|
+
# 🔑 协调者 SummarizationToolMiddleware 内部 middleware 引用:
|
|
229
|
+
# resume_compactor 恢复前分段压缩复用其压缩链(与 DeepAgent 对齐)。
|
|
230
|
+
self.summarization_mw = summarization_mw
|
|
227
231
|
|
|
228
232
|
@property
|
|
229
233
|
def is_downgraded(self) -> bool:
|
|
@@ -304,6 +308,13 @@ class MultiAgentTeam:
|
|
|
304
308
|
if msg is None:
|
|
305
309
|
continue
|
|
306
310
|
|
|
311
|
+
# 🔑 跳过 middleware 内部模型调用的输出(lc_internal_call)。
|
|
312
|
+
# 与 deep_agent 对齐:协调者/子 agent 的 deepagents 内置
|
|
313
|
+
# SummarizationMiddleware 生成摘要时,其内部模型调用的输出
|
|
314
|
+
# 会混进 messages 流 —— 不过滤会把「摘要本体」当回答发给用户。
|
|
315
|
+
if isinstance(metadata, dict) and "lc_internal_call" in metadata:
|
|
316
|
+
continue
|
|
317
|
+
|
|
307
318
|
# 检测 agent 切换
|
|
308
319
|
subagent_name = self._detect_agent_change(namespace, metadata, current_agent)
|
|
309
320
|
if subagent_name and subagent_name != current_agent:
|
|
@@ -680,7 +691,7 @@ async def create_multi_agent_team(
|
|
|
680
691
|
|
|
681
692
|
middleware = [
|
|
682
693
|
_make_guard(detect=False),
|
|
683
|
-
ToolResultTruncationMiddleware(),
|
|
694
|
+
ToolResultTruncationMiddleware(model_name=config.model_name),
|
|
684
695
|
TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
|
|
685
696
|
summarization_mw,
|
|
686
697
|
ModelRequestPrepMiddleware(), # 紧跟 summarization:摘要重定位 + 动态压 max_tokens
|
|
@@ -746,7 +757,7 @@ async def create_multi_agent_team(
|
|
|
746
757
|
name=coord_name,
|
|
747
758
|
middleware=[
|
|
748
759
|
coord_guard,
|
|
749
|
-
ToolResultTruncationMiddleware(),
|
|
760
|
+
ToolResultTruncationMiddleware(model_name=config.model_name),
|
|
750
761
|
TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
|
|
751
762
|
coord_summarization_mw,
|
|
752
763
|
SkillWriteGuardMiddleware(),
|
|
@@ -767,6 +778,7 @@ async def create_multi_agent_team(
|
|
|
767
778
|
downgrade_state=shared_downgrade_state,
|
|
768
779
|
sensitive_guard=coord_guard,
|
|
769
780
|
raw_model=raw_model,
|
|
781
|
+
summarization_mw=getattr(coord_summarization_mw, "_summarization", None),
|
|
770
782
|
)
|
|
771
783
|
|
|
772
784
|
|