sycommon-python-lib 0.2.3a11__py3-none-any.whl → 0.2.4__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.
@@ -105,6 +105,18 @@ from sycommon.agent.chat_events import (
105
105
  error_event,
106
106
  cancelled_event,
107
107
  )
108
+ from sycommon.agent.mcp import (
109
+ MCPToolStatus,
110
+ MCPServerConfig,
111
+ MCPServerCreateRequest,
112
+ MCPServerUpdateRequest,
113
+ MCPServerTestRequest,
114
+ MCPServerTestResult,
115
+ load_mcp_tools,
116
+ test_mcp_connection,
117
+ sanitize_name,
118
+ )
119
+ from sycommon.database.pg_checkpoint_service import PgCheckpointService
108
120
 
109
121
  __all__ = [
110
122
  # 沙箱
@@ -147,4 +159,18 @@ __all__ = [
147
159
  "done_event",
148
160
  "error_event",
149
161
  "cancelled_event",
162
+
163
+ # MCP 工具集成
164
+ "MCPToolStatus",
165
+ "MCPServerConfig",
166
+ "MCPServerCreateRequest",
167
+ "MCPServerUpdateRequest",
168
+ "MCPServerTestRequest",
169
+ "MCPServerTestResult",
170
+ "load_mcp_tools",
171
+ "test_mcp_connection",
172
+ "sanitize_name",
173
+
174
+ # PG Checkpoint 服务
175
+ "PgCheckpointService",
150
176
  ]
@@ -168,6 +168,7 @@ class DeepAgent:
168
168
  """
169
169
  current_tool_calls = []
170
170
  ai_chunk_buffer = ""
171
+ ai_text_content = ""
171
172
  seen_tool_call_ids = set()
172
173
  stream_step = 0
173
174
  # 兜底:累积流式 chunk 中的 usage_metadata(middleware 在流式场景可能拿不到)
@@ -211,6 +212,12 @@ class DeepAgent:
211
212
  if usage_meta:
212
213
  total_input_tokens += usage_meta.get("input_tokens", 0)
213
214
  total_output_tokens += usage_meta.get("output_tokens", 0)
215
+ if usage_meta.get("input_tokens", 0) > 0:
216
+ SYLogger.debug(
217
+ f"[DeepAgent] usage_metadata | input={usage_meta.get('input_tokens', 0)} "
218
+ f"output={usage_meta.get('output_tokens', 0)} "
219
+ f"total={usage_meta.get('total_tokens', 0)} "
220
+ f"cumulative_input={total_input_tokens} step={stream_step}")
214
221
 
215
222
  if msg_type == "AIMessageChunk":
216
223
  tool_calls_log = getattr(msg, "tool_calls", [])
@@ -397,6 +404,7 @@ class DeepAgent:
397
404
 
398
405
  if content:
399
406
  ai_chunk_buffer += content
407
+ ai_text_content += content
400
408
  event = ChatEventBuilder.ai_chunk(
401
409
  content, id=getattr(msg, "id", None),
402
410
  agent=DEFAULT_AGENT_NAME)
@@ -453,6 +461,13 @@ class DeepAgent:
453
461
  print(
454
462
  f"[DeepAgent] AI chunk done | {repr(ai_chunk_buffer[:100])}...")
455
463
 
464
+ # 空响应检测:模型被调用但没有产出任何文本
465
+ if not ai_text_content and not ai_chunk_buffer:
466
+ SYLogger.warning(
467
+ f"[DeepAgent] 空响应警告:模型未返回任何文本内容。"
468
+ f"stream_step={stream_step}, tool_calls={len(current_tool_calls)}, "
469
+ f"input_tokens={total_input_tokens}, output_tokens={total_output_tokens}")
470
+
456
471
  # 兜底:如果 middleware 没有成功记录(流式场景),在这里补充记录
457
472
  if total_input_tokens > 0 or total_output_tokens > 0:
458
473
  try:
@@ -598,8 +613,12 @@ async def create_deep_agent(
598
613
 
599
614
  # 创建 checkpointer
600
615
  if checkpointer is None:
601
- from langgraph.checkpoint.memory import MemorySaver
602
- checkpointer = MemorySaver()
616
+ from sycommon.database.pg_checkpoint_service import PgCheckpointService
617
+ if PgCheckpointService.is_initialized():
618
+ checkpointer = await PgCheckpointService.get_checkpointer()
619
+ else:
620
+ from langgraph.checkpoint.memory import MemorySaver
621
+ checkpointer = MemorySaver()
603
622
 
604
623
  tid = thread_id or user_id
605
624
  agent_config = {"configurable": {"thread_id": tid}}
@@ -0,0 +1,30 @@
1
+ """MCP 工具集成模块
2
+
3
+ 提供 MCP 服务器连接、工具发现和加载功能。
4
+ """
5
+
6
+ from sycommon.agent.mcp.models import (
7
+ MCPToolStatus,
8
+ MCPServerConfig,
9
+ MCPServerCreateRequest,
10
+ MCPServerUpdateRequest,
11
+ MCPServerTestRequest,
12
+ MCPServerTestResult,
13
+ )
14
+ from sycommon.agent.mcp.tool_loader import (
15
+ load_mcp_tools,
16
+ test_mcp_connection,
17
+ sanitize_name,
18
+ )
19
+
20
+ __all__ = [
21
+ "MCPToolStatus",
22
+ "MCPServerConfig",
23
+ "MCPServerCreateRequest",
24
+ "MCPServerUpdateRequest",
25
+ "MCPServerTestRequest",
26
+ "MCPServerTestResult",
27
+ "load_mcp_tools",
28
+ "test_mcp_connection",
29
+ "sanitize_name",
30
+ ]
@@ -0,0 +1,56 @@
1
+ """MCP 服务器配置数据模型"""
2
+
3
+ from typing import Optional, List
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class MCPToolStatus(BaseModel):
8
+ """MCP 工具可用状态"""
9
+ tool_name: str
10
+ description: Optional[str] = None
11
+ available: bool = True
12
+ last_check_at: Optional[str] = None
13
+ last_error: Optional[str] = None
14
+
15
+
16
+ class MCPServerConfig(BaseModel):
17
+ id: str
18
+ user_id: Optional[str] = None
19
+ name: str
20
+ server_url: str
21
+ description: Optional[str] = None
22
+ headers: Optional[dict] = None
23
+ enabled: bool = True
24
+ sanitized_name: str = ""
25
+ created_at: str
26
+ updated_at: str
27
+ tools: Optional[List[MCPToolStatus]] = None
28
+ server_status: Optional[str] = None
29
+ server_error: Optional[str] = None
30
+
31
+
32
+ class MCPServerCreateRequest(BaseModel):
33
+ name: str
34
+ server_url: str
35
+ description: Optional[str] = None
36
+ headers: Optional[dict] = None
37
+ enabled: bool = True
38
+
39
+
40
+ class MCPServerUpdateRequest(BaseModel):
41
+ name: Optional[str] = None
42
+ server_url: Optional[str] = None
43
+ description: Optional[str] = None
44
+ headers: Optional[dict] = None
45
+ enabled: Optional[bool] = None
46
+
47
+
48
+ class MCPServerTestRequest(BaseModel):
49
+ server_url: str
50
+ headers: Optional[dict] = None
51
+
52
+
53
+ class MCPServerTestResult(BaseModel):
54
+ success: bool
55
+ tools: Optional[list] = None
56
+ error: Optional[str] = None
@@ -0,0 +1,174 @@
1
+ """MCP 工具加载器
2
+
3
+ 使用 langchain-mcp-adapters 官方库连接远程 MCP 服务器,
4
+ 发现工具并作为 LangChain BaseTool 注入 Agent。
5
+ """
6
+
7
+ import re
8
+ import hashlib
9
+ from typing import List, Optional, Callable, Awaitable
10
+
11
+ from langchain_core.tools import BaseTool
12
+
13
+ from sycommon.logging.kafka_log import SYLogger
14
+ from sycommon.agent.mcp.models import MCPServerConfig
15
+
16
+
17
+ def sanitize_name(name: str) -> str:
18
+ """将名称转为合法的标识符,中文等非ASCII字符做 transliterate"""
19
+ sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name).strip('_')
20
+ if not sanitized:
21
+ h = hashlib.md5(name.encode()).hexdigest()[:8]
22
+ sanitized = f"mcp_{h}"
23
+ return sanitized
24
+
25
+
26
+ def wrap_tool_with_error_handler(
27
+ tool: BaseTool,
28
+ server_config_id: str,
29
+ server_name: str,
30
+ original_tool_name: str,
31
+ on_tool_success: Optional[Callable[[str, str], Awaitable[None]]] = None,
32
+ on_tool_error: Optional[Callable[[str, str, str], Awaitable[None]]] = None,
33
+ ) -> BaseTool:
34
+ """包装 MCP 工具的 coroutine,捕获连接/超时等异常,返回友好的错误信息而非抛出异常。
35
+
36
+ Args:
37
+ on_tool_success: 异步回调 (server_config_id, tool_name) -> None
38
+ on_tool_error: 异步回调 (server_config_id, tool_name, error_msg) -> None
39
+ """
40
+ original_coroutine = tool.coroutine
41
+ tool_name = tool.name
42
+
43
+ async def _safe_coroutine(*args, **kwargs):
44
+ try:
45
+ result = await original_coroutine(*args, **kwargs)
46
+ if on_tool_success:
47
+ try:
48
+ await on_tool_success(server_config_id, original_tool_name)
49
+ except Exception:
50
+ pass
51
+ return result
52
+ except Exception as e:
53
+ err_type = type(e).__name__
54
+ err_msg = str(e)[:500]
55
+ SYLogger.warning(f"[MCP] 工具 '{tool_name}' 调用失败 ({err_type}): {err_msg}")
56
+
57
+ if on_tool_error:
58
+ try:
59
+ await on_tool_error(server_config_id, original_tool_name, f"{err_type}: {err_msg}")
60
+ except Exception:
61
+ pass
62
+
63
+ friendly_msg = (
64
+ f"MCP 工具调用失败:工具 '{tool_name}' (服务器: {server_name}) 当前不可用。\n"
65
+ f"错误类型: {err_type}\n"
66
+ f"可能原因: MCP 服务器未启动、网络不可达或连接超时。\n"
67
+ f"请尝试不使用该工具继续完成任务,或联系管理员检查 MCP 服务 '{server_name}'。"
68
+ )
69
+ return [friendly_msg]
70
+
71
+ tool.coroutine = _safe_coroutine
72
+ return tool
73
+
74
+
75
+ async def load_mcp_tools(
76
+ configs: List[MCPServerConfig],
77
+ on_tool_success: Optional[Callable[[str, str], Awaitable[None]]] = None,
78
+ on_tool_error: Optional[Callable[[str, str, str], Awaitable[None]]] = None,
79
+ on_batch_available: Optional[Callable[[str, list], Awaitable[None]]] = None,
80
+ on_server_failure: Optional[Callable[[str, str], Awaitable[None]]] = None,
81
+ ) -> List[BaseTool]:
82
+ """加载 MCP 工具列表
83
+
84
+ 接受 MCPServerConfig 列表,逐个服务器连接并加载工具。
85
+
86
+ Args:
87
+ configs: MCP 服务器配置列表(仅 enabled 的会被处理)
88
+ on_tool_success: 单个工具调用成功回调
89
+ on_tool_error: 单个工具调用失败回调
90
+ on_batch_available: 服务器连接成功后批量标记工具可用回调 (config_id, tool_names)
91
+ on_server_failure: 服务器连接失败回调 (config_id, error_msg)
92
+ """
93
+ enabled_configs = [c for c in configs if c.enabled]
94
+ if not enabled_configs:
95
+ return []
96
+
97
+ all_tools = []
98
+ for config in enabled_configs:
99
+ try:
100
+ from langchain_mcp_adapters.client import MultiServerMCPClient
101
+
102
+ key = sanitize_name(config.name)
103
+ client_config = {
104
+ key: {
105
+ "url": config.server_url,
106
+ "transport": "streamable_http",
107
+ **({"headers": config.headers} if config.headers else {}),
108
+ }
109
+ }
110
+
111
+ client = MultiServerMCPClient(client_config)
112
+ tools = await client.get_tools()
113
+
114
+ original_names = []
115
+ for tool in tools:
116
+ original_name = tool.name
117
+ original_names.append(original_name)
118
+ tool.name = f"mcp__{key}__{original_name}"
119
+ if tool.description and not tool.description.startswith("[MCP"):
120
+ tool.description = f"[MCP:{config.name}] {tool.description}"
121
+ wrap_tool_with_error_handler(
122
+ tool, config.id, config.name, original_name,
123
+ on_tool_success=on_tool_success,
124
+ on_tool_error=on_tool_error,
125
+ )
126
+
127
+ all_tools.extend(tools)
128
+
129
+ if on_batch_available:
130
+ try:
131
+ await on_batch_available(config.id, original_names)
132
+ except Exception as e:
133
+ SYLogger.warning(f"[MCP] 写入工具状态失败: {e}")
134
+
135
+ SYLogger.info(f"[MCP] 服务器 '{config.name}' 加载了 {len(tools)} 个工具")
136
+
137
+ except Exception as e:
138
+ SYLogger.warning(f"[MCP] 服务器 '{config.name}' ({config.server_url}) 连接失败,跳过: {e}")
139
+ if on_server_failure:
140
+ try:
141
+ await on_server_failure(config.id, str(e)[:300])
142
+ except Exception:
143
+ pass
144
+
145
+ if all_tools:
146
+ SYLogger.info(f"[MCP] 共加载 {len(all_tools)} 个 MCP 工具")
147
+ return all_tools
148
+
149
+
150
+ async def test_mcp_connection(server_url: str, headers: dict = None) -> dict:
151
+ """测试 MCP 服务器连接,返回发现的工具列表"""
152
+ try:
153
+ from langchain_mcp_adapters.client import MultiServerMCPClient
154
+
155
+ client_config = {
156
+ "test": {
157
+ "url": server_url,
158
+ "transport": "streamable_http",
159
+ **({"headers": headers} if headers else {}),
160
+ }
161
+ }
162
+
163
+ client = MultiServerMCPClient(client_config)
164
+ tools = await client.get_tools()
165
+
166
+ return {
167
+ "success": True,
168
+ "tools": [{"name": t.name, "description": t.description or ""} for t in tools],
169
+ }
170
+ except Exception as e:
171
+ return {
172
+ "success": False,
173
+ "error": str(e),
174
+ }
@@ -574,8 +574,12 @@ async def create_multi_agent_team(
574
574
 
575
575
  # 创建 checkpointer
576
576
  if checkpointer is None:
577
- from langgraph.checkpoint.memory import MemorySaver
578
- checkpointer = MemorySaver()
577
+ from sycommon.database.pg_checkpoint_service import PgCheckpointService
578
+ if PgCheckpointService.is_initialized():
579
+ checkpointer = await PgCheckpointService.get_checkpointer()
580
+ else:
581
+ from langgraph.checkpoint.memory import MemorySaver
582
+ checkpointer = MemorySaver()
579
583
 
580
584
  tid = thread_id or user_id
581
585
  agent_config = {"configurable": {"thread_id": tid}}
@@ -37,6 +37,11 @@ except ImportError:
37
37
  _SSL_CONTEXT = ssl.create_default_context()
38
38
 
39
39
 
40
+ def _read_file_safe(path: str) -> bytes:
41
+ with open(path, "rb") as f:
42
+ return f.read()
43
+
44
+
40
45
  class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
41
46
  """
42
47
  通过 HTTP API 连接远程沙箱容器
@@ -210,6 +215,24 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
210
215
  nacos_group=group,
211
216
  )
212
217
 
218
+ @classmethod
219
+ async def afrom_nacos(
220
+ cls,
221
+ service_name: str,
222
+ user_id: str,
223
+ group: str = "DEFAULT_GROUP",
224
+ version: str = None,
225
+ timeout: int = 180,
226
+ sync_dirs: List[tuple[str, str]] = None,
227
+ auto_sync: bool = False,
228
+ load_balance: bool = True,
229
+ ) -> "HTTPSandboxBackend":
230
+ """异步从 Nacos 服务发现创建后端实例"""
231
+ return await asyncio.to_thread(
232
+ cls.from_nacos,
233
+ service_name, user_id, group, version, timeout, sync_dirs, auto_sync, load_balance,
234
+ )
235
+
213
236
  # ============== 内部方法 - 同步版本 ==============
214
237
 
215
238
  def _refresh_from_nacos_and_switch_sync(self) -> bool:
@@ -565,7 +588,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
565
588
  else:
566
589
  # 单文件
567
590
  SYLogger.info(f"[Sandbox] 上传单文件: {local_path}")
568
- content = await asyncio.to_thread(lambda: open(local_dir, "rb").read())
591
+ content = await asyncio.to_thread(lambda p: _read_file_safe(p), local_dir)
569
592
  upload_results = await self.aupload_files([(remote_path, content)], timeout=timeout)
570
593
  if upload_results[0].error:
571
594
  results[local_path] = {"success": 0, "failed": 1, "errors": [{"path": remote_path, "error": upload_results[0].error}]}
@@ -669,7 +692,8 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
669
692
  def _read_batch():
670
693
  items = []
671
694
  for sandbox_path, local_file in batch_files:
672
- content = open(local_file, "rb").read()
695
+ with open(local_file, "rb") as f:
696
+ content = f.read()
673
697
  items.append((sandbox_path, content))
674
698
  return items
675
699
  batch_items = await asyncio.to_thread(_read_batch)
@@ -1037,7 +1061,7 @@ class HTTPSandboxBackend(FileOperationsMixin, SandboxBackendProtocol):
1037
1061
  SYLogger.info(f"[Sandbox] 异步目录上传完成: {local_path}, 成功={result['success']}, 失败={result['failed']}")
1038
1062
  else:
1039
1063
  SYLogger.info(f"[Sandbox] 异步上传单文件: {local_path}")
1040
- content = await asyncio.to_thread(lambda: open(local_dir, "rb").read())
1064
+ content = await asyncio.to_thread(lambda p: _read_file_safe(p), local_dir)
1041
1065
  upload_results = await self.aupload_files([(remote_path, content)], timeout=timeout)
1042
1066
  if upload_results[0].error:
1043
1067
  results[local_path] = {"success": 0, "failed": 1, "errors": [{"path": remote_path, "error": upload_results[0].error}]}
@@ -162,7 +162,7 @@ class MinioSyncService(metaclass=SingletonMeta):
162
162
  return None
163
163
 
164
164
  def get_presigned_url(self, object_key: str, expires_days: int = 7) -> Optional[str]:
165
- """生成预签名下载 URL"""
165
+ """生成预签名下载 URL(同步)"""
166
166
  if not self._client:
167
167
  return None
168
168
  try:
@@ -177,8 +177,12 @@ class MinioSyncService(metaclass=SingletonMeta):
177
177
  f"[MinIO] Presigned URL failed: {object_key}, error={e}")
178
178
  return None
179
179
 
180
+ async def aget_presigned_url(self, object_key: str, expires_days: int = 7) -> Optional[str]:
181
+ """异步生成预签名下载 URL"""
182
+ return await asyncio.to_thread(self.get_presigned_url, object_key, expires_days)
183
+
180
184
  def remove_object(self, object_key: str) -> bool:
181
- """从 MinIO 删除文件"""
185
+ """从 MinIO 删除文件(同步)"""
182
186
  if not self._client:
183
187
  return False
184
188
  try:
@@ -189,12 +193,12 @@ class MinioSyncService(metaclass=SingletonMeta):
189
193
  SYLogger.warning(f"[MinIO] Delete failed: {object_key}, error={e}")
190
194
  return False
191
195
 
192
- def remove_prefix(self, prefix: str) -> int:
193
- """删除 MinIO 中指定前缀下的所有对象(用于目录删除)
196
+ async def aremove_object(self, object_key: str) -> bool:
197
+ """异步从 MinIO 删除文件"""
198
+ return await asyncio.to_thread(self.remove_object, object_key)
194
199
 
195
- Returns:
196
- 删除的对象数量
197
- """
200
+ def remove_prefix(self, prefix: str) -> int:
201
+ """删除 MinIO 中指定前缀下的所有对象(同步,用于目录删除)"""
198
202
  if not self._client:
199
203
  return 0
200
204
  try:
@@ -210,6 +214,10 @@ class MinioSyncService(metaclass=SingletonMeta):
210
214
  SYLogger.warning(f"[MinIO] Delete prefix failed: {prefix}, error={e}")
211
215
  return 0
212
216
 
217
+ async def aremove_prefix(self, prefix: str) -> int:
218
+ """异步删除 MinIO 中指定前缀下的所有对象"""
219
+ return await asyncio.to_thread(self.remove_prefix, prefix)
220
+
213
221
  # ============== 查找最近副本 ==============
214
222
 
215
223
  def find_latest_object_key(self, user_id: str, file_path: str) -> Optional[str]:
@@ -252,7 +260,7 @@ class MinioSyncService(metaclass=SingletonMeta):
252
260
  SYLogger.warning(f"[MinIO] find_latest failed: {e}")
253
261
  return None
254
262
 
255
- async def afinf_latest_object_key(self, user_id: str, file_path: str) -> Optional[str]:
263
+ async def afind_latest_object_key(self, user_id: str, file_path: str) -> Optional[str]:
256
264
  """异步查找某个文件最近一天的 object key"""
257
265
  return await asyncio.to_thread(self.find_latest_object_key, user_id, file_path)
258
266
 
@@ -473,12 +481,17 @@ class MinioSyncService(metaclass=SingletonMeta):
473
481
  files_to_upload = []
474
482
  for rel_path in batch:
475
483
  try:
476
- response = await asyncio.to_thread(
477
- self._client.get_object, self._bucket, f"{prefix}{rel_path}"
484
+ def _download_object(bucket, key):
485
+ resp = self._client.get_object(bucket, key)
486
+ try:
487
+ return resp.read()
488
+ finally:
489
+ resp.close()
490
+ resp.release_conn()
491
+
492
+ content = await asyncio.to_thread(
493
+ _download_object, self._bucket, f"{prefix}{rel_path}"
478
494
  )
479
- content = response.read()
480
- response.close()
481
- response.release_conn()
482
495
  files_to_upload.append((f"/{rel_path}", content))
483
496
  except Exception as e:
484
497
  SYLogger.warning(f"[MinIO] 下载文件失败: {rel_path}, error={e}")
@@ -123,7 +123,7 @@ class SandboxRecoveryManager:
123
123
  bool: 切换成功返回 True,否则返回 False
124
124
  """
125
125
  # 优先使用底层已有的切换逻辑(会尝试迁移工作空间文件)
126
- switched = await self.backend._refresh_from_nacos_and_switch()
126
+ switched = await asyncio.to_thread(self.backend._refresh_from_nacos_and_switch_sync)
127
127
 
128
128
  if switched:
129
129
  self._consecutive_failures = 0
@@ -149,7 +149,8 @@ class SandboxRecoveryManager:
149
149
  SYLogger.error("[Sandbox] NacosService 未初始化")
150
150
  return False
151
151
 
152
- instances = nacos_manager.get_service_instances(
152
+ instances = await asyncio.to_thread(
153
+ nacos_manager.get_service_instances,
153
154
  self.backend._nacos_service_name,
154
155
  group=self.backend._nacos_group
155
156
  )
@@ -1,8 +1,9 @@
1
1
  """上下文压缩 middleware 构建工具。
2
2
 
3
3
  根据 nacos 中配置的模型 maxTokens,用绝对 token 数设置压缩阈值。
4
- 中文场景下 count_tokens_approximately 默认 chars_per_token=4.0 会低估约 2.1 倍,
5
- 因此传入自定义 token_counter 修正偏差,并按真实 token 数设定阈值。
4
+ 优先使用模型 API 返回的 usage_metadata.total_tokens(真实 token 数),
5
+ metadata 时回退到 chars_per_token=2.0 的估算值。
6
+ 同时增加基于消息数的安全阈值,防止估算偏低导致压缩不触发。
6
7
  """
7
8
 
8
9
  from __future__ import annotations
@@ -24,17 +25,28 @@ if TYPE_CHECKING:
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
27
- # 中文场景修正:chars_per_token 从 4.0 改为 2.0,使近似计数接近真实 token 数
28
- _chinese_token_counter = functools.partial(
29
- count_tokens_approximately, chars_per_token=2.0)
28
+
29
+ def _extract_last_usage_total_tokens(messages) -> int:
30
+ """从消息历史中提取最后一条 AIMessage 的 usage_metadata.total_tokens。
31
+
32
+ 返回 0 表示无数据(需回退到估算)。
33
+ """
34
+ from langchain_core.messages import AIMessage
35
+ for msg in reversed(messages):
36
+ if isinstance(msg, AIMessage):
37
+ meta = getattr(msg, 'usage_metadata', None)
38
+ if meta and isinstance(meta, dict):
39
+ total = meta.get('total_tokens', 0)
40
+ if isinstance(total, int) and total > 0:
41
+ return total
42
+ return 0
30
43
 
31
44
 
32
45
  def _patched_compute_summarization_defaults(model):
33
46
  """覆盖 deepagents 默认值,返回中文场景修正后的绝对 token 阈值。
34
47
 
35
- 不走 fraction 路径(fraction 依赖 model.profile.max_input_tokens,
36
- 但内置 middleware 用默认 chars_per_token=4.0 计数,fraction 基于的基数不准)。
37
- 直接返回修正后的绝对 token 数。
48
+ 同时增加基于消息数的安全阈值:即使 token 估算偏低,
49
+ 消息数超过 200 条时也会触发压缩(覆盖工具 schema 等未计入的开销)。
38
50
  """
39
51
  try:
40
52
  from sycommon.config.Config import Config
@@ -48,10 +60,12 @@ def _patched_compute_summarization_defaults(model):
48
60
  except Exception:
49
61
  max_tokens = 72000
50
62
 
51
- trigger = int(max_tokens * 0.85)
52
- keep = int(max_tokens * 0.15)
63
+ # 60% 触发(120K/200K):实测模型在 input≈137K 时开始退化,
64
+ # 120K 触发压缩留 ~17K 安全余量给工具 schema 等未计入开销
65
+ trigger = int(max_tokens * 0.60)
66
+ keep = int(max_tokens * 0.10)
53
67
  return {
54
- "trigger": ("tokens", trigger),
68
+ "trigger": [("tokens", trigger), ("messages", 200)],
55
69
  "keep": ("tokens", keep),
56
70
  "truncate_args_settings": {
57
71
  "trigger": ("tokens", trigger),
@@ -63,10 +77,14 @@ def _patched_compute_summarization_defaults(model):
63
77
  # monkey-patch:替换 deepagents 的默认计算函数
64
78
  _summ_mod.compute_summarization_defaults = _patched_compute_summarization_defaults
65
79
 
66
- # monkey-patch:在内置 middleware 的 awrap_model_call 中注入日志
80
+ # monkey-patch:在内置 middleware 的 awrap_model_call 中注入真实 token + 日志
67
81
  _OrigDeepAgentsSumm = _summ_mod._DeepAgentsSummarizationMiddleware
68
82
  _orig_awrap_model_call = _OrigDeepAgentsSumm.awrap_model_call
69
83
 
84
+ # 基础估算函数,用于日志对比
85
+ _approx_counter = functools.partial(
86
+ count_tokens_approximately, chars_per_token=2.0)
87
+
70
88
 
71
89
  async def _patched_awrap_model_call(self, request, handler):
72
90
  effective_messages = self._get_effective_messages(request)
@@ -75,15 +93,43 @@ async def _patched_awrap_model_call(self, request, handler):
75
93
  )
76
94
  counted_messages = [request.system_message, *
77
95
  truncated_messages] if request.system_message is not None else truncated_messages
96
+
97
+ # 从截断前的 effective_messages 提取真实 token(截断会丢失 usage_metadata)
98
+ real_tokens = _extract_last_usage_total_tokens(effective_messages)
99
+
100
+ # 估算值(用于日志对比)
78
101
  try:
79
- total_tokens = self.token_counter(
80
- counted_messages, tools=request.tools)
102
+ estimated = _approx_counter(counted_messages, tools=request.tools)
81
103
  except TypeError:
82
- total_tokens = self.token_counter(counted_messages)
83
- should = self._should_summarize(truncated_messages, total_tokens)
104
+ estimated = _approx_counter(counted_messages)
105
+
106
+ # 如果有真实 token,临时替换 token_counter 使 _orig 内部判断也用真实值
107
+ # 这样 _should_summarize 和 _determine_cutoff_index 都能拿到正确的 token 数
108
+ if real_tokens > 0:
109
+ original_counter = self.token_counter
110
+
111
+ def _real_counter(msgs, **kwargs):
112
+ # 优先从当前消息中提取真实值(压缩后的消息可能有新的 metadata)
113
+ r = _extract_last_usage_total_tokens(msgs)
114
+ return r if r > 0 else real_tokens
115
+
116
+ self._lc_helper.token_counter = _real_counter
117
+ try:
118
+ result = await _orig_awrap_model_call(self, request, handler)
119
+ finally:
120
+ self._lc_helper.token_counter = original_counter
121
+ else:
122
+ result = await _orig_awrap_model_call(self, request, handler)
123
+
124
+ # 日志
125
+ source = 'real' if real_tokens > 0 else 'estimated'
126
+ should = self._should_summarize(truncated_messages, real_tokens if real_tokens > 0 else estimated)
84
127
  print(
85
- f"[TokenCount] tokens={total_tokens} msgs={len(counted_messages)} should_summarize={should}")
86
- return await _orig_awrap_model_call(self, request, handler)
128
+ f"[TokenCount] real={real_tokens} estimated={estimated} "
129
+ f"source={source} msgs={len(counted_messages)} "
130
+ f"should_summarize={should} "
131
+ f"trigger={getattr(self._lc_helper, 'trigger', '?')}")
132
+ return result
87
133
 
88
134
 
89
135
  _OrigDeepAgentsSumm.awrap_model_call = _patched_awrap_model_call
@@ -94,25 +140,21 @@ def build_summarization_middleware(
94
140
  model_name: str,
95
141
  backend: "BACKEND_TYPES",
96
142
  *,
97
- trigger_fraction: float = 0.85,
98
- keep_fraction: float = 0.15,
143
+ trigger_fraction: float = 0.60,
144
+ keep_fraction: float = 0.10,
99
145
  default_max_tokens: int = 200000,
100
146
  ) -> SummarizationToolMiddleware:
101
147
  """根据模型上下文窗口大小构建 compact_conversation 工具 middleware。
102
148
 
103
- 自动压缩由 deepagents 内置的 SummarizationMiddleware 处理,
104
- 它通过 model.profile.max_input_tokens(由 LLMWithTokenTracking nacos 配置注入)
105
- 计算压缩阈值。
106
-
107
- 此处创建的 SummarizationToolMiddleware 仅提供 compact_conversation 手动压缩工具,
108
- 使用 nacos 配置的 maxTokens 作为其内部 SummarizationMiddleware 的阈值参数。
149
+ 优先使用模型返回的 usage_metadata 真实 token 数进行压缩判断,
150
+ usage_metadata 时回退到 chars_per_token=2.0 估算。
109
151
 
110
152
  Args:
111
153
  model: LLM 实例。
112
154
  model_name: 模型名称(用于从 nacos 读取配置)。
113
155
  backend: 后端实例。
114
- trigger_fraction: 触发压缩占有效输入的比例,默认 85%。
115
- keep_fraction: 压缩后保留占有效输入的比例,默认 15%。
156
+ trigger_fraction: 触发压缩占有效输入的比例,默认 60%。
157
+ keep_fraction: 压缩后保留占有效输入的比例,默认 10%。
116
158
  default_max_tokens: 无法从配置读取时的默认上下文窗口大小。
117
159
 
118
160
  Returns:
@@ -134,7 +176,8 @@ def build_summarization_middleware(
134
176
  backend=backend,
135
177
  trigger=("tokens", trigger_tokens),
136
178
  keep=("tokens", keep_tokens),
137
- token_counter=_chinese_token_counter,
179
+ token_counter=functools.partial(
180
+ count_tokens_approximately, chars_per_token=2.0),
138
181
  trim_tokens_to_summarize=None,
139
182
  truncate_args_settings={
140
183
  "trigger": ("tokens", trigger_tokens),
@@ -0,0 +1,54 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class PgConfig(BaseModel):
5
+ """PostgreSQL 配置"""
6
+ host: str = Field(default="localhost", description="PG 主机地址")
7
+ port: int = Field(default=5432, description="PG 端口")
8
+ dbname: str = Field(default="postgres", description="数据库名")
9
+ user: str = Field(default="postgres", description="用户名")
10
+ password: str = Field(default="", description="密码")
11
+ min_pool_size: int = Field(default=2, description="连接池最小连接数")
12
+ max_pool_size: int = Field(default=10, description="连接池最大连接数")
13
+
14
+ @classmethod
15
+ def from_dict(cls, config: dict) -> "PgConfig":
16
+ """从字典解析配置
17
+
18
+ 支持两种格式:
19
+ 1. 扁平格式: {"host": "...", "port": 5432, ...}
20
+ 2. Spring 格式: {"spring": {"datasource": {"url": "jdbc:postgresql://...", ...}}}
21
+ """
22
+ spring = config.get("spring", {})
23
+ if spring:
24
+ ds = spring.get("datasource", {})
25
+ url = ds.get("url", "")
26
+ return cls._from_jdbc_url(url, ds.get("username", ""), ds.get("password", ""))
27
+
28
+ return cls(
29
+ host=config.get("host", "localhost"),
30
+ port=config.get("port", 5432),
31
+ dbname=config.get("dbname", config.get("database", "postgres")),
32
+ user=config.get("user", config.get("username", "postgres")),
33
+ password=config.get("password", ""),
34
+ min_pool_size=config.get("min_pool_size", 2),
35
+ max_pool_size=config.get("max_pool_size", 10),
36
+ )
37
+
38
+ @classmethod
39
+ def _from_jdbc_url(cls, url: str, username: str, password: str) -> "PgConfig":
40
+ """从 JDBC URL 解析配置"""
41
+ # jdbc:postgresql://host:port/dbname
42
+ if "://" in url:
43
+ url = url.split("://", 1)[1]
44
+ parts = url.split("/")
45
+ dbname = parts[1] if len(parts) > 1 else "postgres"
46
+ host_port = parts[0].split(":")
47
+ host = host_port[0]
48
+ port = int(host_port[1]) if len(host_port) > 1 else 5432
49
+ return cls(host=host, port=port, dbname=dbname, user=username, password=password)
50
+
51
+ @property
52
+ def dsn(self) -> str:
53
+ """生成 psycopg 连接字符串"""
54
+ return f"host={self.host} port={self.port} dbname={self.dbname} user={self.user} password={self.password}"
@@ -1,6 +1,21 @@
1
1
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
2
2
  from sqlalchemy import text
3
3
 
4
+ # Fix: aiomysql's AsyncAdapt ping() requires 'reconnect' positional arg,
5
+ # but SQLAlchemy's pymysql dialect calls ping() without it.
6
+ # aiomysql asserts reconnect must be False (async reconnection is handled by pool).
7
+ from sqlalchemy.dialects.mysql.pymysql import MySQLDialect_pymysql
8
+
9
+ _original_do_ping = MySQLDialect_pymysql.do_ping
10
+
11
+ def _patched_do_ping(self, dbapi_connection):
12
+ try:
13
+ return _original_do_ping(self, dbapi_connection)
14
+ except TypeError:
15
+ dbapi_connection.ping(reconnect=False)
16
+
17
+ MySQLDialect_pymysql.do_ping = _patched_do_ping
18
+
4
19
  from sycommon.config.Config import SingletonMeta
5
20
  from sycommon.config.DatabaseConfig import DatabaseConfig, convert_dict_keys
6
21
  from sycommon.logging.kafka_log import SYLogger
@@ -0,0 +1,132 @@
1
+ """
2
+ PostgreSQL Checkpoint 服务
3
+
4
+ 为 LangGraph Agent 提供 PostgreSQL 持久化 checkpoint 存储。
5
+ 使用 AsyncPostgresSaver,适配 FastAPI 的异步环境。
6
+
7
+ 用法:
8
+ # 通过 Services.plugins 自动初始化
9
+ Services.plugins(app, pg_checkpoint_service=True)
10
+
11
+ # 或手动初始化
12
+ await PgCheckpointService.setup({"host": "10.10.6.203", "port": 5432, ...})
13
+
14
+ # 获取 checkpointer
15
+ checkpointer = await PgCheckpointService.get_checkpointer()
16
+ """
17
+ import logging
18
+ from typing import Optional, Any
19
+
20
+ from sycommon.config.Config import SingletonMeta
21
+ from sycommon.config.PgConfig import PgConfig
22
+
23
+
24
+ class PgCheckpointService(metaclass=SingletonMeta):
25
+ """PostgreSQL Checkpoint 服务(单例)"""
26
+ _checkpointer: Optional[Any] = None
27
+ _initialized: bool = False
28
+ _config: Optional[PgConfig] = None
29
+ _pool: Optional[Any] = None
30
+
31
+ def __init__(self):
32
+ pass
33
+
34
+ @classmethod
35
+ async def setup(cls, config: Optional[dict] = None):
36
+ """初始化 PG Checkpoint 服务
37
+
38
+ Args:
39
+ config: 配置字典。不传则尝试从 Nacos 读取 pg.yml。
40
+ """
41
+ if cls._initialized:
42
+ return
43
+
44
+ try:
45
+ if config is None:
46
+ try:
47
+ from sycommon.config.Config import Config
48
+ config = Config().config.get('llm', {}).get('PostgreSQL')
49
+ if not config:
50
+ logging.info("未从 Nacos 获取到 PostgreSQL 配置,PG Checkpoint 服务将禁用")
51
+ return
52
+ logging.info("从 Nacos 获取 PostgreSQL 配置成功")
53
+ except Exception as e:
54
+ logging.info(f"从 Nacos 读取 PostgreSQL 配置失败: {e},PG Checkpoint 服务将禁用")
55
+ return
56
+
57
+ cls._config = PgConfig.from_dict(config)
58
+
59
+ from psycopg_pool import AsyncConnectionPool
60
+ from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
61
+
62
+ conn_string = cls._config.dsn
63
+
64
+ cls._pool = AsyncConnectionPool(
65
+ conninfo=conn_string,
66
+ min_size=cls._config.min_pool_size,
67
+ max_size=cls._config.max_pool_size,
68
+ open=False,
69
+ kwargs={
70
+ "autocommit": True,
71
+ "prepare_threshold": 0,
72
+ "keepalives": 1,
73
+ "keepalives_idle": 30,
74
+ "keepalives_interval": 10,
75
+ "keepalives_count": 5,
76
+ },
77
+ check=AsyncConnectionPool.check_connection,
78
+ )
79
+
80
+ await cls._pool.open()
81
+
82
+ cls._checkpointer = AsyncPostgresSaver(cls._pool)
83
+ await cls._checkpointer.setup()
84
+
85
+ cls._initialized = True
86
+ logging.info(
87
+ f"PgCheckpointService 初始化成功,地址: {cls._config.host}:{cls._config.port}/{cls._config.dbname}"
88
+ )
89
+
90
+ except Exception as e:
91
+ logging.error(f"PgCheckpointService 初始化失败: {e}", exc_info=True)
92
+ # 清理可能已创建的连接池
93
+ if cls._pool:
94
+ try:
95
+ await cls._pool.close()
96
+ except Exception:
97
+ pass
98
+ cls._pool = None
99
+ cls._checkpointer = None
100
+
101
+ @classmethod
102
+ async def get_checkpointer(cls) -> Any:
103
+ """获取 AsyncPostgresSaver 实例
104
+
105
+ Returns:
106
+ AsyncPostgresSaver 实例
107
+
108
+ Raises:
109
+ RuntimeError: 如果服务未初始化
110
+ """
111
+ if not cls._initialized or not cls._checkpointer:
112
+ raise RuntimeError("PgCheckpointService 未初始化,请先调用 PgCheckpointService.setup()")
113
+ return cls._checkpointer
114
+
115
+ @classmethod
116
+ def is_initialized(cls) -> bool:
117
+ """检查是否已初始化"""
118
+ return cls._initialized
119
+
120
+ @classmethod
121
+ async def close(cls):
122
+ """关闭 PG 连接池"""
123
+ if cls._pool:
124
+ try:
125
+ await cls._pool.close()
126
+ logging.info("PgCheckpointService 连接池已关闭")
127
+ except Exception as e:
128
+ logging.error(f"关闭 PgCheckpointService 连接池失败: {e}")
129
+ finally:
130
+ cls._pool = None
131
+ cls._checkpointer = None
132
+ cls._initialized = False
sycommon/llm/get_llm.py CHANGED
@@ -168,6 +168,10 @@ def get_llm(
168
168
  "stream_chunk_timeout": 180,
169
169
  }
170
170
 
171
+ # 传入 maxOutputTokens(max_completion_tokens)确保模型有足够的输出 token 空间
172
+ if llmConfig.maxOutputTokens:
173
+ init_params["max_tokens"] = llmConfig.maxOutputTokens
174
+
171
175
  # 合并其他透传参数(包括 presence_penalty, extra_body, top_p 等)
172
176
  init_params.update(kwargs)
173
177
 
sycommon/services.py CHANGED
@@ -20,6 +20,7 @@ from sycommon.synacos.feign import close_all_feign_sessions
20
20
  from sycommon.synacos.feign_client import close_all_feign_client_sessions
21
21
  from sycommon.database.elasticsearch_service import ElasticsearchService
22
22
  from sycommon.database.redis_service import RedisService
23
+ from sycommon.database.pg_checkpoint_service import PgCheckpointService
23
24
  from sycommon.database.database_service import DatabaseService
24
25
  from sycommon.xxljob.xxljob_service import XxlJobService
25
26
 
@@ -109,6 +110,7 @@ class Services(metaclass=SingletonMeta):
109
110
  # 可插拔服务开关(从类变量中继承 plugins() 阶段的设置)
110
111
  self._enable_es: bool = Services.__dict__.get('_enable_es', False)
111
112
  self._enable_redis: bool = Services.__dict__.get('_enable_redis', False)
113
+ self._enable_pg: bool = Services.__dict__.get('_enable_pg', False)
112
114
  self._enable_sandbox: bool = Services.__dict__.get('_enable_sandbox', False)
113
115
  self._enable_xxljob: bool = Services.__dict__.get('_enable_xxljob', False)
114
116
 
@@ -148,6 +150,7 @@ class Services(metaclass=SingletonMeta):
148
150
  rabbitmq_senders: Optional[List[RabbitMQSendConfig]] = None,
149
151
  elasticsearch_service: Optional[Callable[[dict], None]] = None,
150
152
  redis_service: Optional[Callable[[dict], None]] = None,
153
+ pg_checkpoint_service: Optional[Union[bool, dict]] = None,
151
154
  sandbox_service: bool = False,
152
155
  deep_agent_service: bool = False,
153
156
  multi_agent_service: bool = False,
@@ -193,6 +196,7 @@ class Services(metaclass=SingletonMeta):
193
196
  # 保存可插拔服务开关状态到类变量(实例创建时会被拷贝到实例变量)
194
197
  Services._enable_es = elasticsearch_service is not None
195
198
  Services._enable_redis = redis_service is not None
199
+ Services._enable_pg = pg_checkpoint_service is not None
196
200
  Services._enable_sandbox = sandbox_service or deep_agent_service or multi_agent_service
197
201
  Services._enable_xxljob = xxljob_service is not None
198
202
 
@@ -297,14 +301,19 @@ class Services(metaclass=SingletonMeta):
297
301
 
298
302
  app_instance.state.services = instance
299
303
 
300
- # 4. 启动沙箱后台清理任务(如果沙箱服务启用)
304
+ # 4. 初始化 PG Checkpoint 服务(如果启用)
305
+ if instance._enable_pg:
306
+ pg_config = pg_checkpoint_service if isinstance(pg_checkpoint_service, dict) else None
307
+ await PgCheckpointService.setup(pg_config)
308
+
309
+ # 5. 启动沙箱后台清理任务(如果沙箱服务启用)
301
310
  if instance._enable_sandbox:
302
311
  from sycommon.middleware.sandbox import _cleanup_finished_processes
303
312
  sandbox_cleanup_task = asyncio.create_task(
304
313
  _cleanup_finished_processes())
305
314
  logging.info("沙箱后台清理任务已启动")
306
315
 
307
- # 5. 执行用户定义的生命周期
316
+ # 6. 执行用户定义的生命周期
308
317
  if cls._user_lifespan:
309
318
  async with cls._user_lifespan(app_instance):
310
319
  yield
@@ -472,6 +481,13 @@ class Services(metaclass=SingletonMeta):
472
481
  except Exception as e:
473
482
  logging.debug(f"关闭 RedisService 时发生异常: {e}")
474
483
 
484
+ # 关闭 PG Checkpoint 服务(仅当启用时)
485
+ if cls._instance and cls._instance._enable_pg:
486
+ try:
487
+ await PgCheckpointService.close()
488
+ except Exception as e:
489
+ logging.debug(f"关闭 PgCheckpointService 时发生异常: {e}")
490
+
475
491
  # 关闭 XXL-JOB 执行器(仅当启用时)
476
492
  if cls._instance and cls._instance._enable_xxljob:
477
493
  try:
sycommon/synacos/feign.py CHANGED
@@ -203,8 +203,11 @@ async def _feign_internal(service_name, api_path, method='GET', params=None, hea
203
203
  raise TypeError(f"files 参数必须是字典或列表,实际为 {type(files)}")
204
204
  if file_path:
205
205
  filename = os.path.basename(file_path)
206
- with open(file_path, 'rb') as f:
207
- data.add_field('file', f, filename=filename)
206
+ def _read_file():
207
+ with open(file_path, 'rb') as f:
208
+ return f.read()
209
+ content = await asyncio.to_thread(_read_file)
210
+ data.add_field('file', content, filename=filename)
208
211
  # 移除Content-Type,让aiohttp自动处理
209
212
  headers.pop('Content-Type', None)
210
213
  async with session.request(
@@ -303,10 +303,13 @@ def feign_client(
303
303
  for path in file_paths:
304
304
  if not os.path.exists(path):
305
305
  raise FileNotFoundError(f"文件不存在: {path}")
306
- with open(path, "rb") as f:
307
- form_data.add_field(
308
- meta.field_name, f.read(), filename=os.path.basename(path)
309
- )
306
+ def _read_file(p: str) -> bytes:
307
+ with open(p, "rb") as f:
308
+ return f.read()
309
+ content = await asyncio.to_thread(_read_file, path)
310
+ form_data.add_field(
311
+ meta.field_name, content, filename=os.path.basename(path)
312
+ )
310
313
  # 处理表单字段(支持 Pydantic 模型)
311
314
  form_params = {
312
315
  n: m for n, m in param_meta.items() if isinstance(m, Form)}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.3a11
3
+ Version: 0.2.4
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -8,17 +8,18 @@ Requires-Dist: aio-pika>=9.6.2
8
8
  Requires-Dist: aiohttp>=3.13.5
9
9
  Requires-Dist: aiomysql>=0.3.2
10
10
  Requires-Dist: anyio>=4.12.1
11
- Requires-Dist: decorator>=5.2.1
11
+ Requires-Dist: decorator>=5.3.0
12
12
  Requires-Dist: deepagents>=0.6.1
13
13
  Requires-Dist: elasticsearch>=9.4.0
14
14
  Requires-Dist: fastapi>=0.136.1
15
15
  Requires-Dist: jinja2>=3.1.6
16
16
  Requires-Dist: kafka-python>=2.3.1
17
- Requires-Dist: langchain>=1.3.0
17
+ Requires-Dist: langchain>=1.3.1
18
18
  Requires-Dist: langchain-core>=1.4.0
19
19
  Requires-Dist: langchain-openai>=1.2.1
20
20
  Requires-Dist: langfuse>=4.6.1
21
21
  Requires-Dist: langgraph>=1.2.0
22
+ Requires-Dist: langgraph-checkpoint-postgres>=3.1.0
22
23
  Requires-Dist: langgraph-checkpoint-redis>=0.4.1
23
24
  Requires-Dist: ldap3>=2.9.1
24
25
  Requires-Dist: loguru>=0.7.3
@@ -28,17 +29,19 @@ Requires-Dist: psutil>=7.2.2
28
29
  Requires-Dist: pyxxl>=0.4.6
29
30
  Requires-Dist: pydantic>=2.13.4
30
31
  Requires-Dist: python-dotenv>=1.2.2
31
- Requires-Dist: python-multipart>=0.0.28
32
+ Requires-Dist: python-multipart>=0.0.29
32
33
  Requires-Dist: pyyaml>=6.0.3
33
34
  Requires-Dist: redis>=7.3.0
34
35
  Requires-Dist: sentry-sdk[fastapi]>=2.60.0
35
36
  Requires-Dist: sqlalchemy[asyncio]>=2.0.48
36
37
  Requires-Dist: starlette[full]>=1.0.0
37
- Requires-Dist: tiktoken>=0.12.0
38
- Requires-Dist: uvicorn>=0.46.0
38
+ Requires-Dist: tiktoken>=0.13.0
39
+ Requires-Dist: uvicorn>=0.47.0
39
40
  Requires-Dist: wecom-aibot-python-sdk>=1.0.2
40
41
  Requires-Dist: twine>=6.2.0
41
42
  Requires-Dist: minio>=7.2.20
43
+ Requires-Dist: langchain-mcp-adapters>=0.2.2
44
+ Requires-Dist: psycopg[binary,pool]>=3.3.4
42
45
 
43
46
  # sycommon-python-lib
44
47
 
@@ -125,19 +125,22 @@ sycli/rl/strategy_generator.py,sha256=RzYkaj4jQ_5aNOuj1WA-SXIHfO5h1nTSSHmLZpO-gi
125
125
  sycli/rl/strategy_prompts.py,sha256=dwx291OnyywP6z7uKmoDPZNCC4MNrIyDANdAEO0evJs,2873
126
126
  sycli/skills/__init__.py,sha256=0o9HuaDSvN-z0JZwlDSwU7WxtchQn7f0OtluIMgtC0Q,212
127
127
  sycommon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
- sycommon/services.py,sha256=N6Z4D-qmdyGBZ5z72j2dtSfpXnrnWjEuSyKCgSIY0Cs,21332
129
- sycommon/agent/__init__.py,sha256=PXxUiDwdzxv3WQfD8R5MKXtV7qZPvbRc0wPxnS-ZsNQ,3752
128
+ sycommon/services.py,sha256=UtWn36dPmB9g1CTKkNgzbU8M28iOkZd2BtdhvDZLfbk,22214
129
+ sycommon/agent/__init__.py,sha256=mxceAeUifQ-DKvWp7ZEJIFlmOCb5wpYHPGQw3rwEN8I,4378
130
130
  sycommon/agent/agent_manager.py,sha256=UhhaekEumT7g4v_Z1UB4jTp13X0n8M8erYaQdkGGWkA,13620
131
131
  sycommon/agent/chat_events.py,sha256=bWAMWYIZ2L_yqUcn5jq9ius_lQxLHEv4zQLEqX6UaeM,13190
132
- sycommon/agent/deep_agent.py,sha256=B0nDtpZBAP5bikIKopDKl7l1RJRHQcfCyH3FBx6IWgI,30470
133
- sycommon/agent/multi_agent_team.py,sha256=6Ba4MbdEDVuXe6iBE_Q84kzr5zJYLo_djdpkkwJhShg,26558
134
- sycommon/agent/summarization_utils.py,sha256=9LCvt3AcH07JBowtIqv3fFEspXl-BQebDdZHe4DX3-k,5646
132
+ sycommon/agent/deep_agent.py,sha256=WTGKWx0MxmiIbbrKPTbQl6on2w4LQRWtH0hdiViA-dE,31709
133
+ sycommon/agent/multi_agent_team.py,sha256=NHmsUNwe3huguUUzbeoiOjgo9wh4-eXH0AjPDvm1dP4,26781
134
+ sycommon/agent/summarization_utils.py,sha256=PRCIFtYBrH0bbSxsIc-qpC4iEXJzk72UuR7u5mQTt2w,7360
135
+ sycommon/agent/mcp/__init__.py,sha256=iKrdDhIrFsNIkqG_kgcwNe-nOiM6uVfolKv44LfQ-FQ,636
136
+ sycommon/agent/mcp/models.py,sha256=RBAIbGETNXkqD3wQZT7eKS4ozkgE9DQEneF1WKZf1C0,1355
137
+ sycommon/agent/mcp/tool_loader.py,sha256=SEny14f7Bm9I17pT-9PJWMbhi9Ki77wvCR0KRNEJmyM,6428
135
138
  sycommon/agent/sandbox/__init__.py,sha256=jR7LlkD4J4Y6QYyRXQClkwmqDBCCPmycV_hQV9p9YHw,4621
136
139
  sycommon/agent/sandbox/file_ops.py,sha256=6ymRMM0WchM7G_YmF1ckrLjf5s_JCh1wrAp2g_-sg8k,23162
137
- sycommon/agent/sandbox/http_sandbox_backend.py,sha256=mjiTZnADvUq_rO05ewllo_eGDS4uTdD2e2GGYvBpF-Q,56150
138
- sycommon/agent/sandbox/minio_sync.py,sha256=r6tjoQA8AHNVG_hcHS3enfFnw-eTkW4r7jA7bwatsWc,19241
140
+ sycommon/agent/sandbox/http_sandbox_backend.py,sha256=kwuPEmrOMyxfrRu20AEGqWD9t38L-DrtKSFp6CWt44o,56877
141
+ sycommon/agent/sandbox/minio_sync.py,sha256=d1kuWllvyAvAMsFZCP0OdHEQtXN9BEIgHbupC31BjSk,20000
139
142
  sycommon/agent/sandbox/sandbox_pool.py,sha256=eMn8sLakCWf90l6ni2-333QM8oBdX1CflV-WzneFp_k,9133
140
- sycommon/agent/sandbox/sandbox_recovery.py,sha256=VDhFI1q9DzSs5B3s2gee1mTmXQoxs0UCXzDrqNQ7VBY,7295
143
+ sycommon/agent/sandbox/sandbox_recovery.py,sha256=X-eDODx1tmGMh_iTngV6e1ppfDBHpTdkPreJusN5MHY,7358
141
144
  sycommon/agent/sandbox/session.py,sha256=TjzC3yFC-VaJ75UwCyL26QX4PRTGNNfQae1FKFuOsYI,2365
142
145
  sycommon/auth/__init__.py,sha256=W814cfHlLXFymmxeTi3pIreFb4nhKnQ7NY1H38x1Gic,974
143
146
  sycommon/auth/ldap_service.py,sha256=fOcpVov5LWJkBk62qbTaltks1c4la7JsbD104KfdBOI,10102
@@ -149,16 +152,18 @@ sycommon/config/EmbeddingConfig.py,sha256=gPKwiDYbeu1GpdIZXMmgqM7JqBIzCXi0yYuGRL
149
152
  sycommon/config/LLMConfig.py,sha256=pjMiUgsUaKHw6WNi3weL2ilE9nFua5MkXg96kgwtuzY,510
150
153
  sycommon/config/LangfuseConfig.py,sha256=t2LulAtnMUvIINOKHXNWlT5PtgNb7IuaHURjWlbma38,370
151
154
  sycommon/config/MQConfig.py,sha256=_RDcmIdyWKjmgM5ZnriOoI-DpaxgXs7CD0awdAD6z88,252
155
+ sycommon/config/PgConfig.py,sha256=Hs9LwgIxSBxcFP16oq18N6Gq9hU2qVl4-7bPfd-ON_s,2333
152
156
  sycommon/config/RedisConfig.py,sha256=gIa4BS8L_HdmBg9Dkv3cuIK6CU9zt9RodZOJUuUlh5Y,5235
153
157
  sycommon/config/RerankerConfig.py,sha256=35sVwzus2IscvTHnCG63Orl2pC-pMsrVi6wAGDmOH3U,341
154
158
  sycommon/config/SentryConfig.py,sha256=OsLb3G9lTsCSZ7tWkcXWJHmvfILQopBxje5pjnkFJfo,320
155
159
  sycommon/config/XxlJobConfig.py,sha256=VSG6dn9ysfUVunOs7PqugyZUGJWmX_cEePz2ZCfqHtU,392
156
160
  sycommon/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
161
  sycommon/database/async_base_db_service.py,sha256=w6ONUiTtF4-bXRnkBt9QpL9BAy0XUDbQG7F9Hf2rfjw,1337
158
- sycommon/database/async_database_service.py,sha256=HZSV0ntVTteT-VZfkM9dwuld-gN5CY_T3lE5wwUpDck,4307
162
+ sycommon/database/async_database_service.py,sha256=Cf3RaO3skP6IAJrkta-CRE-Q1NtjWLPLUe9hazB8LRM,4873
159
163
  sycommon/database/base_db_service.py,sha256=J5ELHMNeGfzA6zVcASPSPZ0XNKrRY3_gdGmVkZw3Mto,946
160
164
  sycommon/database/database_service.py,sha256=IMoJ9554dYkr6QfRofvNa0VR24U1WQDz_ATrg0-6EQ0,3857
161
165
  sycommon/database/elasticsearch_service.py,sha256=qm490GRlxZlYsQgyfyclSbARRP1-Tc4Lwav3lbPINvQ,3092
166
+ sycommon/database/pg_checkpoint_service.py,sha256=LCwJ9ZADIXkE715Fi3ySkNDDwlY3PC6ZSi34ZYXzBbs,4418
162
167
  sycommon/database/redis_service.py,sha256=tPw8UgeuyYQBxWfPRjx7VqlSRFNxIsnR0WSGd36GaA8,20509
163
168
  sycommon/database/token_usage_db_service.py,sha256=_hoeB4lYPhDOlJLaUIHIl7z-DNpzsRYrPn5oboD1Y38,6254
164
169
  sycommon/health/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -171,7 +176,7 @@ sycommon/heartbeat_process/heartbeat_process_manager.py,sha256=24qUKs8qegdWHqcox
171
176
  sycommon/heartbeat_process/heartbeat_process_worker.py,sha256=duuAEFwda43Y6pZE8tOOspitlyxecaFsg1n1iH9jQDQ,16863
172
177
  sycommon/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
173
178
  sycommon/llm/embedding.py,sha256=Xwmg1HcgbdW7OcpYyzu8k7U-27rC5lzjIqbg221CcyY,19129
174
- sycommon/llm/get_llm.py,sha256=RHfZlbZeuTju9yfWiSIuzwvOyceWICETzZMuAyqwkgo,7376
179
+ sycommon/llm/get_llm.py,sha256=9F2EJhP3ujbk78h1jW8T9Jafb-79U2QAUutYaSbTHvY,7572
175
180
  sycommon/llm/llm_logger.py,sha256=LLXiESwDP5f8dB50nFabShVoLKv8UCf2ll69zo1FOso,3365
176
181
  sycommon/llm/llm_tokens.py,sha256=yGEessxfk5wRMrPGyWHhiiIIQFDVT23FSaqnwqHGCoY,4712
177
182
  sycommon/llm/llm_with_token_tracking.py,sha256=vrdH5LlXg1y2glDPJVawT9A-JoXL9qnvMfHAU3CNy70,12928
@@ -234,8 +239,8 @@ sycommon/sse/sse.py,sha256=OQ3ElV8WCi-AD3-e0nbiUF28Syf6GRpGztneWTn77EM,10356
234
239
  sycommon/synacos/__init__.py,sha256=Re9YKVjL62AZURejgSQ3-OvIiMXY-KeAAjIcRJ8PsO0,329
235
240
  sycommon/synacos/example.py,sha256=FOnBkvodR8WF_jf-RovM3ngVmvZQX6wKwMLscUTGn2M,8707
236
241
  sycommon/synacos/example2.py,sha256=yYuQscfHUIl1HLZ8kSRBuZpHUcNWZMi5H3Mb-LjYnvk,8136
237
- sycommon/synacos/feign.py,sha256=kWnKPyWNUGOTIsmdJvn_Ch7Nm5essjsnX4bS7udmlQQ,12468
238
- sycommon/synacos/feign_client.py,sha256=nD8Ar8n0NOy5wzaWKe_8MxV76-UuuoaBV_Q5BxOriTI,19736
242
+ sycommon/synacos/feign.py,sha256=RU6p2gRP3LZoHYBBEUUY9z5KKzGiUmwj4qdo7aF9UNA,12610
243
+ sycommon/synacos/feign_client.py,sha256=i6O20JWl1g2fjD-et7olFaO-0z2yEbKT8-pKluAHCgs,19917
239
244
  sycommon/synacos/nacos_client_base.py,sha256=iP5kLkBD2VOrx6X8v6_RnC9NWiBWmL6-Bgf493QOtxc,6899
240
245
  sycommon/synacos/nacos_config_manager.py,sha256=Swqsd9X2xO5-x2VfKUrq8HjzRJn8JBPDqyXazWlF-T4,6859
241
246
  sycommon/synacos/nacos_heartbeat_manager.py,sha256=LfimUKpG4KaqsVQl150sg3MLK8psanuUwQ07tjL3uBE,10963
@@ -260,8 +265,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
260
265
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
261
266
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
262
267
  sycommon/xxljob/xxljob_service.py,sha256=JIEJaGXhqrTLcyxlyynSrsHg9bBnDNzX-D4qIWLRPUE,6815
263
- sycommon_python_lib-0.2.3a11.dist-info/METADATA,sha256=KBOLd7w-EjfHtbIxYku--to9cFJrIjieoL3D_KbHDqw,7740
264
- sycommon_python_lib-0.2.3a11.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
265
- sycommon_python_lib-0.2.3a11.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
266
- sycommon_python_lib-0.2.3a11.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
267
- sycommon_python_lib-0.2.3a11.dist-info/RECORD,,
268
+ sycommon_python_lib-0.2.4.dist-info/METADATA,sha256=EvJmf8i4ZkIwoaj0n_KVTUHnAeBGXKF2mI1kjU4EKnI,7877
269
+ sycommon_python_lib-0.2.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
270
+ sycommon_python_lib-0.2.4.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
271
+ sycommon_python_lib-0.2.4.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
272
+ sycommon_python_lib-0.2.4.dist-info/RECORD,,