sycommon-python-lib 0.2.7a55__py3-none-any.whl → 0.2.8__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.
- sycli/cli.py +9 -0
- sycli/commands/acp_serve_cmd.py +113 -0
- sycommon/agent/acp/server_adapter.py +99 -0
- sycommon/agent/deep_agent.py +187 -25
- sycommon/agent/middleware/model_request_prep.py +17 -2
- sycommon/agent/middleware/sensitive_guard.py +4 -0
- sycommon/agent/multi_agent_team.py +21 -5
- sycommon/agent/resume_compactor.py +223 -6
- sycommon/agent/sandbox/file_ops.py +24 -5
- sycommon/agent/sandbox/http_sandbox_backend.py +16 -2
- sycommon/agent/sandbox/minio_sync.py +3 -2
- sycommon/agent/summarization_utils.py +46 -34
- sycommon/auth/wecom_ldap_service.py +154 -15
- sycommon/config/Config.py +46 -12
- sycommon/config/EgressPolicyConfig.py +183 -0
- sycommon/database/async_base_db_service.py +41 -6
- sycommon/database/async_database_service.py +84 -17
- sycommon/llm/sy_langfuse.py +1 -1
- sycommon/llm/token_usage_mysql_service.py +1 -1
- sycommon/logging/kafka_log.py +283 -37
- sycommon/middleware/egress_wall.py +268 -0
- sycommon/middleware/sandbox.py +578 -50
- sycommon/middleware/sandbox_gateway.py +593 -0
- sycommon/middleware/tool_result_truncation.py +34 -6
- sycommon/middleware/traceid.py +30 -1
- sycommon/models/base_http.py +52 -0
- sycommon/models/sandbox.py +44 -1
- sycommon/notice/uvicorn_monitor.py +4 -4
- sycommon/rabbitmq/rabbit_management_client.py +100 -0
- sycommon/rabbitmq/rabbitmq_client.py +182 -4
- sycommon/rabbitmq/rabbitmq_pool.py +141 -63
- sycommon/rabbitmq/rabbitmq_service.py +6 -4
- sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +261 -3
- sycommon/rabbitmq/rabbitmq_service_core.py +72 -19
- sycommon/rabbitmq/rabbitmq_service_producer_manager.py +162 -81
- sycommon/sentry/sy_sentry.py +1 -1
- sycommon/services.py +29 -1
- 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_consumer_broker_health.py +495 -0
- sycommon/tests/test_deep_agent.py +5 -4
- sycommon/tests/test_env_tenant_helper.py +72 -0
- sycommon/tests/test_nacos_llm_real.py +113 -0
- sycommon/tests/test_sandbox_glob_hardening.py +292 -0
- sycommon/tests/test_upgrade_verification.py +7 -7
- sycommon/tools/time_utils.py +75 -0
- sycommon/tools/user_id.py +106 -0
- {sycommon_python_lib-0.2.7a55.dist-info → sycommon_python_lib-0.2.8.dist-info}/METADATA +17 -16
- {sycommon_python_lib-0.2.7a55.dist-info → sycommon_python_lib-0.2.8.dist-info}/RECORD +53 -44
- {sycommon_python_lib-0.2.7a55.dist-info → sycommon_python_lib-0.2.8.dist-info}/WHEEL +1 -1
- sycommon/agent/middleware/sitecustomize.py +0 -300
- sycommon/agent/middleware/skill_api_whitelist.py +0 -440
- sycommon/agent/middleware/skill_wl_check.py +0 -123
- sycommon/config/SkillApiWhitelistConfig.py +0 -87
- {sycommon_python_lib-0.2.7a55.dist-info → sycommon_python_lib-0.2.8.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.7a55.dist-info → sycommon_python_lib-0.2.8.dist-info}/top_level.txt +0 -0
sycli/cli.py
CHANGED
|
@@ -173,6 +173,11 @@ def _create_parser() -> argparse.ArgumentParser:
|
|
|
173
173
|
p_acp.add_argument("--config", "-c", help="Path to ACP config file (default: .sycli/acp.yaml)")
|
|
174
174
|
p_acp.add_argument("--project-root", "-r", default=".", help="Project root directory")
|
|
175
175
|
|
|
176
|
+
# ── acp-serve ──
|
|
177
|
+
p_acp_serve = sub.add_parser(
|
|
178
|
+
"acp-serve", help="Serve a DeepAgent as Zed ACP agent over stdio (LLM-backed)")
|
|
179
|
+
p_acp_serve.add_argument("--project-root", "-r", default=".", help="Project root directory")
|
|
180
|
+
|
|
176
181
|
# ── version ──
|
|
177
182
|
sub.add_parser("version", help="Show version")
|
|
178
183
|
|
|
@@ -243,6 +248,10 @@ def main() -> None:
|
|
|
243
248
|
from sycli.commands.acp_cmd import handle_acp
|
|
244
249
|
sys.exit(handle_acp(args))
|
|
245
250
|
|
|
251
|
+
elif args.command == "acp-serve":
|
|
252
|
+
from sycli.commands.acp_serve_cmd import handle_acp_serve
|
|
253
|
+
sys.exit(handle_acp_serve(args))
|
|
254
|
+
|
|
246
255
|
|
|
247
256
|
def _handle_status(args) -> None:
|
|
248
257
|
"""Show current RL state."""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""sycli acp-serve command — 把 DeepAgent 起成 Zed ACP agent(stdio)。
|
|
2
|
+
|
|
3
|
+
与 `sycli acp` 的区别:
|
|
4
|
+
- acp:把 .sycli/acp.yaml 里的静态 CLI 工具暴露成 ACP agent(无 LLM,纯命令路由)。
|
|
5
|
+
- acp-serve:把一个真的 DeepAgent(LLM + 本地 shell 后端)暴露成 ACP agent,
|
|
6
|
+
Zed/其它 ACP client 连上后即可对话式驱动它干活。
|
|
7
|
+
|
|
8
|
+
stdio 约定:stdout 是 JSON-RPC 通道,一切诊断输出走 stderr。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import signal
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _e(msg: str) -> None:
|
|
19
|
+
"""stderr 诊断输出(stdout 留给 JSON-RPC)。"""
|
|
20
|
+
print(f" {msg}", file=sys.stderr)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handle_acp_serve(args) -> int:
|
|
24
|
+
"""Handle `sycli acp-serve` command.
|
|
25
|
+
|
|
26
|
+
Flow:
|
|
27
|
+
1. Resolve project root, load sycli.json(LLM 配置)
|
|
28
|
+
2. Build DeepAgent graph(LLM + LocalShellBackend)
|
|
29
|
+
3. Serve over stdio as Zed ACP agent (blocking)
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
args: Parsed argparse namespace with project_root.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Exit code (0 for success, 1 for error).
|
|
36
|
+
"""
|
|
37
|
+
project_root = Path(args.project_root or ".").resolve()
|
|
38
|
+
config_path = project_root / ".sycli" / "sycli.json"
|
|
39
|
+
|
|
40
|
+
from sycli.models.config_models import SycliConfig
|
|
41
|
+
|
|
42
|
+
config = SycliConfig.load(config_path)
|
|
43
|
+
issues = config.validate_config()
|
|
44
|
+
fatal = [i for i in issues if "base_url" in i or "api_key" in i]
|
|
45
|
+
if fatal:
|
|
46
|
+
for i in fatal:
|
|
47
|
+
_e(f"✘ Config: {i}")
|
|
48
|
+
_e("✘ LLM 未配置,无法启动 DeepAgent。先运行 `sycli init` 或检查 sycli.json。")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
# Print summary to stderr(stdout 留给 stdio JSON-RPC)
|
|
52
|
+
_e("═" * 60)
|
|
53
|
+
_e("Zed ACP DeepAgent Server")
|
|
54
|
+
_e(f" Project: {project_root}")
|
|
55
|
+
_e(f" Config: {config_path if config_path.exists() else '(defaults)'}")
|
|
56
|
+
_e(f" Model: {config.llm.model} @ {config.llm.resolved_base_url()}")
|
|
57
|
+
_e(" Transport: stdio (JSON-RPC)")
|
|
58
|
+
_e("─" * 60)
|
|
59
|
+
|
|
60
|
+
# Build DeepAgent graph(延迟导入:出错时不污染 stdout)
|
|
61
|
+
def _build_graph():
|
|
62
|
+
from deepagents import create_deep_agent
|
|
63
|
+
from sycli.agents.factory import build_subagents
|
|
64
|
+
from sycli.core.backend import create_backend
|
|
65
|
+
from sycli.core.llm import create_llm
|
|
66
|
+
|
|
67
|
+
llm = create_llm(config.llm, streaming=True)
|
|
68
|
+
backend = create_backend(project_root)
|
|
69
|
+
subagents = build_subagents(config, llm, backend, project_name=project_root.name, mode="serve", project_root=str(project_root))
|
|
70
|
+
return create_deep_agent(
|
|
71
|
+
model=llm,
|
|
72
|
+
backend=backend,
|
|
73
|
+
system_prompt=(
|
|
74
|
+
"你是 sycli DeepAgent,正通过 ACP 协议被远程 client 驱动。"
|
|
75
|
+
"在项目根目录内完成任务;需要拆解任务时用 write_todos 维护计划。"
|
|
76
|
+
),
|
|
77
|
+
subagents=subagents,
|
|
78
|
+
checkpointer=None, # langchain-acp 适配层自动挂进程内 checkpointer
|
|
79
|
+
debug=False,
|
|
80
|
+
name="sycli-acp-serve",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
from sycommon.agent.acp.server_adapter import serve_graph
|
|
84
|
+
|
|
85
|
+
_shutdown = False
|
|
86
|
+
|
|
87
|
+
def _signal_handler(sig, frame):
|
|
88
|
+
nonlocal _shutdown
|
|
89
|
+
if _shutdown:
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
_shutdown = True
|
|
92
|
+
_e("⚠ Shutting down ACP DeepAgent...")
|
|
93
|
+
|
|
94
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
serve_graph(
|
|
98
|
+
graph_factory=_build_graph,
|
|
99
|
+
agent_name="sycli-deepagent",
|
|
100
|
+
agent_title="sycli DeepAgent",
|
|
101
|
+
)
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
_e("⚠ ACP DeepAgent stopped.")
|
|
104
|
+
return 0
|
|
105
|
+
except Exception as e:
|
|
106
|
+
_e(f"✘ ACP server failed: {e}")
|
|
107
|
+
import traceback
|
|
108
|
+
|
|
109
|
+
traceback.print_exc(file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
return 0
|
|
113
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""ACP server 适配器——把 LangGraph/DeepAgent graph 暴露成 ACP agent(stdio)。
|
|
2
|
+
|
|
3
|
+
与 client.py 相反的方向:client.py 让我们的 agent 通过 ACP 调远程 agent(如 Codex);
|
|
4
|
+
本模块让我们的 agent(DeepAgent compiled graph)反过来被 ACP client(Zed 编辑器、
|
|
5
|
+
其它主控 agent)驱动,基于 langchain-acp 的 LangChainAcpAgent 适配层。
|
|
6
|
+
|
|
7
|
+
对外暴露:
|
|
8
|
+
- serve_graph:把 compiled graph 包成 ACP agent 并阻塞跑 stdio JSON-RPC 循环。
|
|
9
|
+
- build_acp_server_agent:只构建 LangChainAcpAgent(不进 IO 循环,测试/嵌入用)。
|
|
10
|
+
|
|
11
|
+
会话与状态:
|
|
12
|
+
- 每 ACP session 对应 graph 一个 thread_id(session_id),多轮上下文由
|
|
13
|
+
langgraph checkpointer 维持;无 checkpointer 的 graph 会被适配层自动挂
|
|
14
|
+
MemorySaver(进程内,进程退出即失)。
|
|
15
|
+
- deepagents 的 write_todos 计划事件经 DeepAgentsCompatibilityBridge 投影成
|
|
16
|
+
ACP AgentPlanUpdate,client(如 Zed)能实时看到计划进度。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Any, Callable, Optional
|
|
23
|
+
|
|
24
|
+
from langchain_acp import create_acp_agent, run_acp
|
|
25
|
+
from langchain_acp.runtime.adapter import LangChainAcpAgent
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# graph 工厂类型:langchain-acp 以 FactoryGraphSource 调用工厂,签名是
|
|
30
|
+
# factory(session: AcpSessionContext) -> compiled graph(session 可忽略)。
|
|
31
|
+
# 声明成可选参以兼容「无参工厂」——我们只透传给 langchain-acp。
|
|
32
|
+
GraphFactory = Callable[..., Any]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _adapt_factory(factory: GraphFactory) -> GraphFactory:
|
|
36
|
+
"""把任意零参工厂包成 langchain-acp 要求的 (session) -> graph 签名。"""
|
|
37
|
+
def _wrapped(session: Any = None) -> Any: # noqa: ARG001
|
|
38
|
+
return factory()
|
|
39
|
+
return _wrapped
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_acp_server_agent(
|
|
43
|
+
graph: Any = None,
|
|
44
|
+
*,
|
|
45
|
+
graph_factory: Optional[GraphFactory] = None,
|
|
46
|
+
agent_name: str = "sycommon-deepagent",
|
|
47
|
+
agent_title: str = "sycommon DeepAgent",
|
|
48
|
+
) -> LangChainAcpAgent:
|
|
49
|
+
"""构建 ACP 化的 agent(不进 IO 循环)。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
graph: 已编译的 LangGraph/DeepAgent graph(与 graph_factory 二选一)。
|
|
53
|
+
graph_factory: 返回 compiled graph 的零参工厂(优先级低于 graph)。
|
|
54
|
+
agent_name: ACP agentInfo.name(client 侧展示用)。
|
|
55
|
+
agent_title: ACP agentInfo.title。
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
LangChainAcpAgent,可直接传给 acp.run_agent 或自托管事件循环。
|
|
59
|
+
"""
|
|
60
|
+
if graph is None and graph_factory is None:
|
|
61
|
+
raise ValueError("graph 与 graph_factory 至少提供一个")
|
|
62
|
+
agent = create_acp_agent(
|
|
63
|
+
graph=graph,
|
|
64
|
+
graph_factory=_adapt_factory(graph_factory) if graph_factory else None,
|
|
65
|
+
config=_make_config(agent_name, agent_title),
|
|
66
|
+
)
|
|
67
|
+
logger.info("[acp_server] agent 就绪 name=%s(等待 stdio client 连接)", agent_name)
|
|
68
|
+
return agent
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def serve_graph(
|
|
72
|
+
graph: Any = None,
|
|
73
|
+
*,
|
|
74
|
+
graph_factory: Optional[GraphFactory] = None,
|
|
75
|
+
agent_name: str = "sycommon-deepagent",
|
|
76
|
+
agent_title: str = "sycommon DeepAgent",
|
|
77
|
+
) -> None:
|
|
78
|
+
"""把 graph 包成 ACP agent 并阻塞运行 stdio JSON-RPC 循环。
|
|
79
|
+
|
|
80
|
+
stdout 是 JSON-RPC 通道,任何日志必须走 stderr(上层负责 logging 配置)。
|
|
81
|
+
client 断开 stdin 或进程被 kill 时返回。
|
|
82
|
+
"""
|
|
83
|
+
agent = build_acp_server_agent(
|
|
84
|
+
graph, graph_factory=graph_factory,
|
|
85
|
+
agent_name=agent_name, agent_title=agent_title,
|
|
86
|
+
)
|
|
87
|
+
run_acp(graph_source=agent._graph_source, config=agent._config)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _make_config(agent_name: str, agent_title: str) -> Any:
|
|
91
|
+
"""构造 AdapterConfig(agent 标识 + deepagents 兼容桥)。"""
|
|
92
|
+
from langchain_acp import AdapterConfig
|
|
93
|
+
from langchain_acp.bridges import DeepAgentsCompatibilityBridge
|
|
94
|
+
|
|
95
|
+
return AdapterConfig(
|
|
96
|
+
agent_name=agent_name,
|
|
97
|
+
agent_title=agent_title,
|
|
98
|
+
capability_bridges=(DeepAgentsCompatibilityBridge(),),
|
|
99
|
+
)
|
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
|
# 默认系统提示词
|
|
@@ -160,6 +211,10 @@ class AgentConfig(BaseModel):
|
|
|
160
211
|
# (scoped 由服务端路由硬编码,user 由 MinIO 按 user 灌入)。
|
|
161
212
|
system_skills_allow: Optional[set[str]] = None
|
|
162
213
|
memory_dir: Optional[str] = None
|
|
214
|
+
# 额外 middleware(应用层注入,如技能使用上报、自定义拦截):
|
|
215
|
+
# 数字员工等项目自定义 AgentMiddleware 实例列表,create_deep_agent 会
|
|
216
|
+
# 追加到内置 middleware_list 之后执行。类型用 list 而非具体类避免循环依赖。
|
|
217
|
+
extra_middleware: list = []
|
|
163
218
|
|
|
164
219
|
@property
|
|
165
220
|
def skills_dir(self) -> Optional[str]:
|
|
@@ -228,6 +283,8 @@ class DeepAgent:
|
|
|
228
283
|
rubric_eval_collector: Optional[_RubricEvalCollector] = None,
|
|
229
284
|
sensitive_guard: Optional[SensitiveGuardMiddleware] = None,
|
|
230
285
|
raw_model: Any = None,
|
|
286
|
+
request_prep: Optional[ModelRequestPrepMiddleware] = None,
|
|
287
|
+
summarization_mw: Any = None,
|
|
231
288
|
):
|
|
232
289
|
self.user_id = user_id
|
|
233
290
|
self.agent = agent
|
|
@@ -236,6 +293,12 @@ class DeepAgent:
|
|
|
236
293
|
self.recovery_manager = recovery_manager
|
|
237
294
|
self.rubric_eval_collector = rubric_eval_collector
|
|
238
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
|
|
239
302
|
# 🔑 存 raw_model 引用,供 cleanup 关 LLM client(root_async_client)。
|
|
240
303
|
# model 被埋进 graph 内部节点闭包,表层拿不到,必须 create 时显式传入。
|
|
241
304
|
self._raw_model = raw_model
|
|
@@ -286,6 +349,13 @@ class DeepAgent:
|
|
|
286
349
|
ai_text_content = ""
|
|
287
350
|
seen_tool_call_ids = set()
|
|
288
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 # 无备忘时默认已完成(跳过判定)
|
|
289
359
|
# 兜底:累积流式 chunk 中的 usage_metadata(middleware 在流式场景可能拿不到)
|
|
290
360
|
total_input_tokens = 0
|
|
291
361
|
total_output_tokens = 0
|
|
@@ -401,6 +471,14 @@ class DeepAgent:
|
|
|
401
471
|
|
|
402
472
|
stream_step += 1
|
|
403
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
|
+
|
|
404
482
|
# ===== 逐 chunk 诊断日志:默认关闭,DEEP_AGENT_STREAM_DEBUG=true 时开启 =====
|
|
405
483
|
if _STREAM_DEBUG:
|
|
406
484
|
_chunk_type = type(chunk).__name__
|
|
@@ -565,6 +643,46 @@ class DeepAgent:
|
|
|
565
643
|
if _cleaned:
|
|
566
644
|
content = content.strip()
|
|
567
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
|
+
|
|
568
686
|
if msg_type in ("AIMessage", "AIMessageChunk"):
|
|
569
687
|
# 🔑 提取 reasoning_content(思考/推理内容)
|
|
570
688
|
# 模型在 thinking 模式下会将推理过程放在 reasoning_content 字段,
|
|
@@ -777,6 +895,28 @@ class DeepAgent:
|
|
|
777
895
|
print(
|
|
778
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)
|
|
779
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
|
+
|
|
780
920
|
SYLogger.warning(
|
|
781
921
|
f"[STREAM-DEBUG] FOR LOOP ENDED | _astream_chunk_count={_astream_chunk_count} | "
|
|
782
922
|
f"stream_step={stream_step} | ai_text_len={len(ai_text_content)} | "
|
|
@@ -1111,13 +1251,17 @@ async def create_deep_agent(
|
|
|
1111
1251
|
|
|
1112
1252
|
middleware_list = [
|
|
1113
1253
|
guard,
|
|
1114
|
-
|
|
1254
|
+
# 截断上限从 nacos maxTokens 推导(×0.04,下限2000);读不到配置兜底 8192
|
|
1255
|
+
ToolResultTruncationMiddleware(model_name=config.model_name),
|
|
1115
1256
|
TokenTrackingMiddleware(model_name=config.model_name, user_id=user_id),
|
|
1116
1257
|
summarization_mw,
|
|
1117
1258
|
ModelRequestPrepMiddleware(), # 紧跟 summarization:摘要重定位 + 动态压 max_tokens
|
|
1118
1259
|
SkillWriteGuardMiddleware(), # 系统 /skills/system 只读守卫(先于白名单)
|
|
1119
1260
|
SandboxPathGuardMiddleware(user_id), # 宿主绝对路径规范化/拦截(防越权读他人沙箱文件)
|
|
1120
1261
|
]
|
|
1262
|
+
# 应用层注入的额外 middleware(如技能使用上报),排在内置之后
|
|
1263
|
+
if config.extra_middleware:
|
|
1264
|
+
middleware_list.extend(config.extra_middleware)
|
|
1121
1265
|
|
|
1122
1266
|
# RubricMiddleware:复用 FilesystemMiddleware 的工具(底层走 HTTPSandboxBackend HTTP 远程调用)
|
|
1123
1267
|
if config.rubric_max_iterations > 0 and sandbox_backend:
|
|
@@ -1130,17 +1274,6 @@ async def create_deep_agent(
|
|
|
1130
1274
|
on_evaluation=rubric_eval_collector.on_evaluation,
|
|
1131
1275
|
))
|
|
1132
1276
|
|
|
1133
|
-
# SkillApiWhitelistMiddleware:按子技能注入接口白名单(拦 execute 工具,
|
|
1134
|
-
# export SKILL_API_WHITELIST + 推送 sitecustomize 到沙箱做 path 级校验)
|
|
1135
|
-
if sandbox_backend:
|
|
1136
|
-
try:
|
|
1137
|
-
from sycommon.agent.middleware.skill_api_whitelist import (
|
|
1138
|
-
SkillApiWhitelistMiddleware)
|
|
1139
|
-
middleware_list.append(
|
|
1140
|
-
SkillApiWhitelistMiddleware(sandbox_backend))
|
|
1141
|
-
except Exception as _wl_err:
|
|
1142
|
-
SYLogger.warning(f"[DeepAgent] 技能接口白名单中间件加载失败(忽略): {_wl_err}")
|
|
1143
|
-
|
|
1144
1277
|
agent_kwargs = {
|
|
1145
1278
|
"model": raw_model,
|
|
1146
1279
|
"tools": config.tools or [get_current_date],
|
|
@@ -1204,6 +1337,15 @@ async def create_deep_agent(
|
|
|
1204
1337
|
rubric_eval_collector=rubric_eval_collector,
|
|
1205
1338
|
sensitive_guard=guard,
|
|
1206
1339
|
raw_model=raw_model,
|
|
1340
|
+
request_prep=next(
|
|
1341
|
+
(m for m in middleware_list
|
|
1342
|
+
if type(m).__name__ == "ModelRequestPrepMiddleware"), None),
|
|
1343
|
+
# SummarizationToolMiddleware 实例引用:resume_compactor 分段压缩复用
|
|
1344
|
+
# 其内部 _DeepAgentsSummarizationMiddleware(_acreate_summary 等)。
|
|
1345
|
+
# graph 内部探测(deepagents 未暴露 middleware 列表)不可靠,创建时显式传入。
|
|
1346
|
+
summarization_mw=next(
|
|
1347
|
+
(getattr(m, "_summarization", None) for m in middleware_list
|
|
1348
|
+
if type(m).__name__ == "SummarizationToolMiddleware"), None),
|
|
1207
1349
|
)
|
|
1208
1350
|
|
|
1209
1351
|
|
|
@@ -1429,6 +1571,8 @@ async def _sync_skills_to_sandbox(
|
|
|
1429
1571
|
|
|
1430
1572
|
skill_count = 0
|
|
1431
1573
|
skip_count = 0
|
|
1574
|
+
# 收集需要重传的技能(版本不一致 / 无版本 / 正文校验失败)
|
|
1575
|
+
pending = [] # [(skill_name, local_version, src, remote_version)]
|
|
1432
1576
|
for skill_name, local_version, src in local_skills:
|
|
1433
1577
|
remote_version = remote_version_map.get(skill_name, "")
|
|
1434
1578
|
|
|
@@ -1437,19 +1581,27 @@ async def _sync_skills_to_sandbox(
|
|
|
1437
1581
|
# (历史上出现过沙箱 SKILL.md 版本号新、正文旧的脏数据,导致 agent
|
|
1438
1582
|
# 永远读不到新版指令)。校验失败则强制走删除+重传。
|
|
1439
1583
|
# fresh_map 在进入循环前已并发预取,这里只查表不再 await。
|
|
1440
|
-
if
|
|
1441
|
-
SYLogger.warning(
|
|
1442
|
-
f"[DeepAgent] 技能版本一致但正文不一致,强制重传: {skill_name} "
|
|
1443
|
-
f"v{local_version}")
|
|
1444
|
-
# 落到下方的删除+重传分支
|
|
1445
|
-
else:
|
|
1584
|
+
if fresh_map.get(skill_name, False):
|
|
1446
1585
|
SYLogger.debug(
|
|
1447
1586
|
f"[DeepAgent] 技能版本一致,跳过: {skill_name} v{local_version}")
|
|
1448
1587
|
backend._synced_skills.add(skill_name)
|
|
1449
1588
|
skip_count += 1
|
|
1450
1589
|
continue
|
|
1451
|
-
|
|
1452
|
-
|
|
1590
|
+
SYLogger.warning(
|
|
1591
|
+
f"[DeepAgent] 技能版本一致但正文不一致,强制重传: {skill_name} "
|
|
1592
|
+
f"v{local_version}")
|
|
1593
|
+
pending.append((skill_name, local_version, src, remote_version))
|
|
1594
|
+
|
|
1595
|
+
# 并发删除 + 全量上传:信号量限流,把「N 次串行 HTTP 上传」压成「N/并发 批」。
|
|
1596
|
+
# 单个技能上传失败仅记 warning、不拖垮其余(原串行版会让一个失败中断后续所有技能)。
|
|
1597
|
+
# 内存安全:技能库总量 ~2.9MB / 最大单文件 ~608KB,base64 编码瞬时膨胀 ~3x,
|
|
1598
|
+
# 6 路并发最坏额外内存峰值 ~10MB 量级,对服务进程可忽略;若未来技能塞入大资源文件,
|
|
1599
|
+
# 可调低此值或改走 aupload_stream_from_disk 流式上传。
|
|
1600
|
+
_SKILL_SYNC_CONCURRENCY = 6
|
|
1601
|
+
_sync_sem = asyncio.Semaphore(_SKILL_SYNC_CONCURRENCY)
|
|
1602
|
+
|
|
1603
|
+
async def _sync_one(skill_name, local_version, src, remote_version):
|
|
1604
|
+
# 删除旧目录(走 /delete,跳过懒同步,毫秒级)
|
|
1453
1605
|
if skill_name in existing_dirs:
|
|
1454
1606
|
deleted_ok = False
|
|
1455
1607
|
for _attempt in range(2): # 最多重试一次,确保旧内容真删掉
|
|
@@ -1473,11 +1625,21 @@ async def _sync_skills_to_sandbox(
|
|
|
1473
1625
|
f"[DeepAgent] 删除旧版技能彻底失败,本次跳过: {skill_name}")
|
|
1474
1626
|
|
|
1475
1627
|
# 全量上传整个技能目录到沙箱(batch_upload 服务端无条件覆盖)
|
|
1476
|
-
|
|
1628
|
+
try:
|
|
1629
|
+
async with _sync_sem:
|
|
1630
|
+
await backend.async_sync_dirs([(src, f"{sandbox_dest}/{skill_name}")])
|
|
1631
|
+
except Exception as e:
|
|
1632
|
+
SYLogger.warning(
|
|
1633
|
+
f"[DeepAgent] 技能上传失败: {skill_name}, {e}")
|
|
1634
|
+
return 0
|
|
1477
1635
|
backend._synced_skills.add(skill_name)
|
|
1478
|
-
skill_count += 1
|
|
1479
1636
|
SYLogger.info(
|
|
1480
1637
|
f"[DeepAgent] 全量同步技能到沙箱: {skill_name} v{local_version}")
|
|
1638
|
+
return 1
|
|
1639
|
+
|
|
1640
|
+
if pending:
|
|
1641
|
+
results = await asyncio.gather(*[_sync_one(*p) for p in pending])
|
|
1642
|
+
skill_count = sum(r for r in results if r)
|
|
1481
1643
|
|
|
1482
1644
|
if chmod_readonly:
|
|
1483
1645
|
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:
|