agentic-kit-common 0.0.17__tar.gz → 0.0.21__tar.gz

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.
Files changed (55) hide show
  1. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/PKG-INFO +3 -1
  2. agentic_kit_common-0.0.21/agentic_kit_common/config/__init__.py +21 -0
  3. agentic_kit_common-0.0.21/agentic_kit_common/config/config_loader.py +311 -0
  4. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/llm/openai.py +2 -2
  5. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/log/logger.py +2 -2
  6. agentic_kit_common-0.0.21/agentic_kit_common/mcp/mcp_client.py +344 -0
  7. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/session.py +2 -2
  8. agentic_kit_common-0.0.21/agentic_kit_common/redis/redis_pool_manager.py +118 -0
  9. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/sms/ali_sms_client.py +2 -2
  10. agentic_kit_common-0.0.21/agentic_kit_common/web/http/__init__.py +0 -0
  11. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common.egg-info/PKG-INFO +3 -1
  12. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common.egg-info/SOURCES.txt +9 -0
  13. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common.egg-info/requires.txt +2 -0
  14. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/setup.py +5 -1
  15. agentic_kit_common-0.0.21/test/__init__.py +0 -0
  16. agentic_kit_common-0.0.21/test/conf.yaml +56 -0
  17. agentic_kit_common-0.0.21/test/test_config_manager.py +29 -0
  18. agentic_kit_common-0.0.21/test/test_mcp_client.py +29 -0
  19. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/README.md +0 -0
  20. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/__init__.py +0 -0
  21. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/llm/__init__.py +0 -0
  22. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/llm/utils.py +0 -0
  23. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/log/__init__.py +0 -0
  24. {agentic_kit_common-0.0.17/agentic_kit_common/minio → agentic_kit_common-0.0.21/agentic_kit_common/mcp}/__init__.py +0 -0
  25. {agentic_kit_common-0.0.17/agentic_kit_common/sms → agentic_kit_common-0.0.21/agentic_kit_common/minio}/__init__.py +0 -0
  26. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/minio/minio_manager.py +0 -0
  27. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/mongodb/__init__.py +0 -0
  28. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/mongodb/mongodb_manager.py +0 -0
  29. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/__init__.py +0 -0
  30. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/base.py +0 -0
  31. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/execution.py +0 -0
  32. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/manager.py +0 -0
  33. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/multi_session.py +0 -0
  34. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/orm/schema.py +0 -0
  35. {agentic_kit_common-0.0.17/agentic_kit_common/vector → agentic_kit_common-0.0.21/agentic_kit_common/redis}/__init__.py +0 -0
  36. {agentic_kit_common-0.0.17/agentic_kit_common/web → agentic_kit_common-0.0.21/agentic_kit_common/sms}/__init__.py +0 -0
  37. {agentic_kit_common-0.0.17/agentic_kit_common/web/http → agentic_kit_common-0.0.21/agentic_kit_common/vector}/__init__.py +0 -0
  38. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/embedding/__init__.py +0 -0
  39. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/embedding/embedding.py +0 -0
  40. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/manager/__init__.py +0 -0
  41. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/manager/milvus_manager.py +0 -0
  42. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/schema/__init__.py +0 -0
  43. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/schema/base.py +0 -0
  44. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/vector/schema/milvus_schema.py +0 -0
  45. {agentic_kit_common-0.0.17/test → agentic_kit_common-0.0.21/agentic_kit_common/web}/__init__.py +0 -0
  46. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common/web/http/response.py +0 -0
  47. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common.egg-info/dependency_links.txt +0 -0
  48. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/agentic_kit_common.egg-info/top_level.txt +0 -0
  49. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/setup.cfg +0 -0
  50. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/test/config.py +0 -0
  51. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/test/settings.py +0 -0
  52. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/test/test_embedding.py +0 -0
  53. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/test/test_minio.py +0 -0
  54. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/test/test_orm.py +0 -0
  55. {agentic_kit_common-0.0.17 → agentic_kit_common-0.0.21}/test/test_vector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-kit-common
3
- Version: 0.0.17
3
+ Version: 0.0.21
4
4
  Summary: Common utilities and tools for agentic kit ecosystem
5
5
  Home-page:
6
6
  Author: manson
@@ -25,12 +25,14 @@ Requires-Dist: langgraph
25
25
  Requires-Dist: langchain_community
26
26
  Requires-Dist: langchain_experimental
27
27
  Requires-Dist: langchain-openai
28
+ Requires-Dist: langchain_mcp_adapters
28
29
  Requires-Dist: mysql-connector-python
29
30
  Requires-Dist: sqlalchemy
30
31
  Requires-Dist: sqlglot
31
32
  Requires-Dist: pymilvus
32
33
  Requires-Dist: xinference_client
33
34
  Requires-Dist: pymongo
35
+ Requires-Dist: redis
34
36
  Requires-Dist: aliyun-python-sdk-core
35
37
  Requires-Dist: aliyun-python-sdk-dysmsapi
36
38
  Dynamic: author
@@ -0,0 +1,21 @@
1
+ from .config_loader import (
2
+ ConfigManager,
3
+ load_config,
4
+ get_llm_config,
5
+ get_rag_config,
6
+ get_database_config,
7
+ get_full_config,
8
+ reload_config,
9
+ set_config_manager,
10
+ )
11
+
12
+ __all__ = [
13
+ "ConfigManager",
14
+ "set_config_manager",
15
+ "load_config",
16
+ "get_llm_config",
17
+ "get_rag_config",
18
+ "get_database_config",
19
+ "get_full_config",
20
+ "reload_config",
21
+ ]
@@ -0,0 +1,311 @@
1
+ import logging
2
+ import os
3
+ from typing import Any, Dict, Optional
4
+
5
+ import yaml
6
+ from dotenv import load_dotenv, find_dotenv
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ load_dotenv(find_dotenv(usecwd=True))
12
+ _config_root_path = os.getenv("DELTA_CONFIG_PATH", None)
13
+
14
+
15
+ class ConfigManager:
16
+ """
17
+ 配置管理器 - 支持多种配置源
18
+
19
+ 支持的配置方式(优先级从高到低):
20
+ 1. 环境变量(最高优先级)
21
+ 2. 初始化时传入的 config 参数
22
+ 3. 配置文件(config_path 或默认的 conf.yaml)
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ config: Optional[Dict[str, Any]] = None,
28
+ config_path: Optional[str] = None
29
+ ):
30
+ """
31
+ 初始化配置管理器
32
+
33
+ Args:
34
+ config: 直接传入的配置字典(适合外部 SDK 调用)
35
+ 格式示例:
36
+ {
37
+ "BASIC_MODEL": {
38
+ "base_url": "http://...",
39
+ "model": "qwen",
40
+ "api_key": "sk-xxx"
41
+ },
42
+ "VISION_MODEL": {
43
+ "base_url": "http://...",
44
+ "model": "vision",
45
+ "api_key": "sk-xxx"
46
+ },
47
+ "RAG_CLIENT": {
48
+ "base_url": "http://...",
49
+ "timeout": 120
50
+ },
51
+ "DATABASE_CLIENT": {
52
+ "base_url": "http://...",
53
+ "timeout": 120
54
+ },
55
+ "MCP_SETTINGS": {
56
+ "default_servers": {
57
+ "file-processor": {
58
+ "transport": "streamable_http",
59
+ "url": "http://...",
60
+ "enabled_tools": ["convert_to_markdown"]
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ config_path: 配置文件路径(可选)
67
+ - 如果不提供,默认使用项目根目录的 conf.yaml
68
+ - 如果提供,使用指定路径的配置文件
69
+
70
+ Examples:
71
+ # 方式1: 直接传入配置(外部 SDK 推荐)
72
+ config_mgr = ConfigManager(config={
73
+ "BASIC_MODEL": {...},
74
+ "RAG_CLIENT": {...}
75
+ })
76
+
77
+ # 方式2: 指定配置文件路径
78
+ config_mgr = ConfigManager(config_path="/path/to/conf.yaml")
79
+
80
+ # 方式3: 使用默认配置文件(项目根目录/conf.yaml)
81
+ config_mgr = ConfigManager()
82
+ """
83
+ self._external_config = config or {}
84
+ self._config_path = config_path or _config_root_path
85
+ self._config_cache: Optional[Dict[str, Any]] = None
86
+
87
+ def load_config(self, force_reload: bool = False) -> Dict[str, Any]:
88
+ """
89
+ 加载配置(带缓存)
90
+
91
+ Args:
92
+ force_reload: 是否强制重新加载
93
+
94
+ Returns:
95
+ Dict[str, Any]: 完整配置字典
96
+ """
97
+ if self._config_cache is not None and not force_reload:
98
+ return self._config_cache
99
+
100
+ # 如果有外部配置,直接使用
101
+ if self._external_config:
102
+ self._config_cache = self._external_config.copy()
103
+ logger.info("使用外部传入的配置")
104
+ return self._config_cache
105
+
106
+ # 从配置文件加载
107
+ conf_path = self._config_path
108
+
109
+ if not os.path.exists(conf_path):
110
+ logger.warning(f"配置文件不存在: {conf_path},使用空配置")
111
+ self._config_cache = {}
112
+ return self._config_cache
113
+
114
+ try:
115
+ with open(conf_path, 'r', encoding='utf-8') as f:
116
+ config = yaml.safe_load(f) or {}
117
+ self._config_cache = config
118
+ logger.info(f"成功加载配置文件: {conf_path}")
119
+ except Exception as e:
120
+ logger.error(f"加载配置文件失败: {e}")
121
+ self._config_cache = {}
122
+
123
+ return self._config_cache
124
+
125
+ def get_llm_config(self, model_type: str = "BASIC_MODEL") -> Dict[str, Any]:
126
+ """
127
+ 获取LLM配置
128
+
129
+ Args:
130
+ model_type: 模型类型,可选 "BASIC_MODEL", "VISION_MODEL"
131
+
132
+ Returns:
133
+ Dict[str, Any]: LLM配置
134
+ """
135
+ config = self.load_config()
136
+ base_config = config.get(model_type, {}).copy() if isinstance(config.get(model_type), dict) else {}
137
+
138
+ # 环境变量覆盖(优先级最高)
139
+ prefix = f"{model_type}__"
140
+ for key, value in os.environ.items():
141
+ if key.startswith(prefix):
142
+ conf_key = key[len(prefix):].lower()
143
+ base_config[conf_key] = value
144
+
145
+ return base_config
146
+
147
+ def get_rag_config(self) -> Dict[str, Any]:
148
+ """
149
+ 获取RAG客户端配置
150
+
151
+ Returns:
152
+ Dict[str, Any]: RAG配置
153
+ """
154
+ config = self.load_config()
155
+ rag_config = config.get("RAG_CLIENT", {}).copy() if isinstance(config.get("RAG_CLIENT"), dict) else {}
156
+
157
+ # 支持环境变量覆盖
158
+ env_overrides = {
159
+ "base_url": os.getenv("RAG_CLIENT__base_url"),
160
+ "timeout": os.getenv("RAG_CLIENT__timeout"),
161
+ "max_retries": os.getenv("RAG_CLIENT__max_retries"),
162
+ }
163
+
164
+ for key, value in env_overrides.items():
165
+ if value is not None:
166
+ rag_config[key] = value
167
+
168
+ return rag_config
169
+
170
+ def get_mcp_config(self) -> Dict[str, Any]:
171
+ """
172
+ 获取MCP服务器配置
173
+
174
+ Returns:
175
+ Dict[str, Any]: MCP配置
176
+ """
177
+ config = self.load_config()
178
+ return config.get("MCP_SETTINGS", {})
179
+
180
+ def get_database_config(self) -> Dict[str, Any]:
181
+ """
182
+ 获取数据库客户端配置
183
+
184
+ Returns:
185
+ Dict[str, Any]: 数据库配置
186
+ """
187
+ config = self.load_config()
188
+ db_config = config.get("DATABASE_CLIENT", {}).copy() if isinstance(config.get("DATABASE_CLIENT"), dict) else {}
189
+
190
+ # 支持环境变量覆盖
191
+ env_overrides = {
192
+ "base_url": os.getenv("DATABASE_CLIENT__base_url"),
193
+ "timeout": os.getenv("DATABASE_CLIENT__timeout"),
194
+ }
195
+
196
+ for key, value in env_overrides.items():
197
+ if value is not None:
198
+ db_config[key] = value
199
+
200
+ return db_config
201
+
202
+ def get_full_config(self) -> Dict[str, Any]:
203
+ """
204
+ 获取完整配置
205
+
206
+ Returns:
207
+ Dict[str, Any]: 完整配置字典
208
+ """
209
+ return self.load_config()
210
+
211
+ def reload_config(self):
212
+ """重新加载配置(清除缓存)"""
213
+ self._config_cache = None
214
+ logger.info("配置已重新加载")
215
+
216
+
217
+ # ============================================================================
218
+ # 全局默认配置管理器(向后兼容)
219
+ # ============================================================================
220
+
221
+ _default_config_manager: Optional[ConfigManager] = None
222
+
223
+
224
+ def _get_default_config_manager() -> ConfigManager:
225
+ """获取默认配置管理器实例"""
226
+ global _default_config_manager
227
+ if _default_config_manager is None:
228
+ _default_config_manager = ConfigManager()
229
+ return _default_config_manager
230
+
231
+
232
+ def set_config_manager(config_manager: ConfigManager):
233
+ """
234
+ 设置全局配置管理器(用于外部 SDK)
235
+
236
+ Args:
237
+ config_manager: ConfigManager 实例
238
+
239
+ Example:
240
+ # 在外部 SDK 中设置自定义配置
241
+ config_mgr = ConfigManager(config={...})
242
+ set_config_manager(config_mgr)
243
+ """
244
+ global _default_config_manager
245
+ _default_config_manager = config_manager
246
+ logger.info("已设置全局配置管理器")
247
+
248
+
249
+ # ============================================================================
250
+ # 向后兼容的函数接口
251
+ # ============================================================================
252
+
253
+ def load_config(force_reload: bool = False) -> Dict[str, Any]:
254
+ """
255
+ 加载配置文件(带缓存)
256
+
257
+ Args:
258
+ force_reload: 是否强制重新加载
259
+
260
+ Returns:
261
+ Dict[str, Any]: 完整配置字典
262
+ """
263
+ return _get_default_config_manager().load_config(force_reload)
264
+
265
+
266
+ def get_llm_config(model_type: str = "BASIC_MODEL") -> Dict[str, Any]:
267
+ """
268
+ 获取LLM配置
269
+
270
+ Args:
271
+ model_type: 模型类型,可选 "BASIC_MODEL", "VISION_MODEL"
272
+
273
+ Returns:
274
+ Dict[str, Any]: LLM配置
275
+ """
276
+ return _get_default_config_manager().get_llm_config(model_type)
277
+
278
+
279
+ def get_rag_config() -> Dict[str, Any]:
280
+ """
281
+ 获取RAG客户端配置
282
+
283
+ Returns:
284
+ Dict[str, Any]: RAG配置
285
+ """
286
+ return _get_default_config_manager().get_rag_config()
287
+
288
+
289
+ def get_database_config() -> Dict[str, Any]:
290
+ """
291
+ 获取数据库客户端配置
292
+
293
+ Returns:
294
+ Dict[str, Any]: 数据库配置
295
+ """
296
+ return _get_default_config_manager().get_database_config()
297
+
298
+
299
+ def get_full_config() -> Dict[str, Any]:
300
+ """
301
+ 获取完整配置
302
+
303
+ Returns:
304
+ Dict[str, Any]: 完整配置字典
305
+ """
306
+ return _get_default_config_manager().get_full_config()
307
+
308
+
309
+ def reload_config():
310
+ """重新加载配置(清除缓存)"""
311
+ _get_default_config_manager().reload_config()
@@ -1,9 +1,9 @@
1
1
  import os
2
2
 
3
- from dotenv import load_dotenv
3
+ from dotenv import load_dotenv, find_dotenv
4
4
  from langchain_openai import ChatOpenAI
5
5
 
6
- load_dotenv()
6
+ load_dotenv(find_dotenv(usecwd=True))
7
7
 
8
8
 
9
9
  model_name = os.getenv("MODEL_NAME", None)
@@ -5,9 +5,9 @@ from logging.handlers import TimedRotatingFileHandler
5
5
  from pathlib import Path
6
6
  from typing import Union
7
7
 
8
- from dotenv import load_dotenv
8
+ from dotenv import load_dotenv, find_dotenv
9
9
 
10
- load_dotenv()
10
+ load_dotenv(find_dotenv(usecwd=True))
11
11
 
12
12
 
13
13
  logger_root_path = os.getenv("LOGGER_ROOT_PATH", None)
@@ -0,0 +1,344 @@
1
+ import logging
2
+ from typing import Dict, Any, List, Optional, TYPE_CHECKING
3
+
4
+ from langchain_mcp_adapters.client import MultiServerMCPClient
5
+ from langchain_core.tools import BaseTool
6
+
7
+ # if TYPE_CHECKING:
8
+ # from src.core.config import ConfigManager
9
+
10
+ logger = logging.getLogger(__name__)
11
+ logger.setLevel(logging.ERROR)
12
+
13
+
14
+ class MCPToolsNotAvailableError(Exception):
15
+ """MCP工具不可用异常"""
16
+ pass
17
+
18
+
19
+ class MCPClient:
20
+ """
21
+ MCP (Model Context Protocol) 客户端
22
+
23
+ 支持的配置方式(优先级从高到低):
24
+ 1. 初始化时传入的 config 参数
25
+ 2. 配置管理器 (config_manager)
26
+ 3. 默认配置文件(项目根目录/conf.yaml)
27
+
28
+ Examples:
29
+ # 方式1: 直接传入配置(外部 SDK 推荐)
30
+ client = MCPClient(config={
31
+ "default_servers": {
32
+ "web_search": {
33
+ "url": "http://your-server.com/mcp",
34
+ "transport": "streamable_http",
35
+ "enabled_tools": ["web_search", "crawl_page"] # 可选,不传则使用所有工具
36
+ }
37
+ }
38
+ })
39
+
40
+ # 方式2: 使用配置管理器
41
+ from src.core.config import ConfigManager
42
+ config_mgr = ConfigManager(config={
43
+ "MCP_SETTINGS": {
44
+ "default_servers": {...}
45
+ }
46
+ })
47
+ client = MCPClient(config_manager=config_mgr)
48
+
49
+ # 方式3: 使用默认配置文件(conf.yaml)
50
+ client = MCPClient()
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ config: Optional[Dict[str, Any]] = None,
56
+ config_manager: Optional['ConfigManager'] = None
57
+ ):
58
+ """
59
+ 初始化MCP客户端
60
+
61
+ Args:
62
+ config: 直接传入的配置字典(可选)
63
+ 格式示例:
64
+ {
65
+ "default_servers": {
66
+ "server_name": {
67
+ "url": "http://...",
68
+ "transport": "sse" | "streamable_http",
69
+ "enabled_tools": ["tool1", "tool2"] # 可选,不传则使用所有工具
70
+ }
71
+ }
72
+ }
73
+ config_manager: 配置管理器实例(可选)
74
+ """
75
+ assert not (config is None and config_manager is None), "config 与 config_manager 不能同时为 None"
76
+
77
+ self._config = config
78
+ self._config_manager = config_manager
79
+ self._client: Optional[MultiServerMCPClient] = None
80
+ self._initialized = False
81
+ self._enabled_tools_map: Dict[str, List[str]] = {} # 服务器名 -> 启用的工具列表
82
+ self._server_tools_cache: Dict[str, List[BaseTool]] = {} # 服务器名 -> 工具列表缓存
83
+
84
+ def _get_mcp_config(self) -> Dict[str, Any]:
85
+ """
86
+ 获取 MCP 配置
87
+
88
+ Returns:
89
+ Dict[str, Any]: MCP 配置
90
+ """
91
+ if self._config:
92
+ return self._config
93
+
94
+ if self._config_manager:
95
+ return self._config_manager.get_mcp_config()
96
+
97
+ async def initialize(self):
98
+ """初始化MCP客户端"""
99
+ if self._initialized:
100
+ return
101
+
102
+ try:
103
+ mcp_config = self._get_mcp_config()
104
+ default_servers = mcp_config.get('default_servers', {})
105
+
106
+ if not default_servers:
107
+ logger.warning("配置文件中没有MCP服务器")
108
+ return
109
+
110
+ logger.info(f"开始初始化 {len(default_servers)} 个MCP服务器...")
111
+
112
+ server_configs = {}
113
+ for server_name, server_config in default_servers.items():
114
+ url = server_config.get('url')
115
+ transport = server_config.get('transport', 'sse')
116
+ enabled_tools = server_config.get('enabled_tools')
117
+
118
+ if not url:
119
+ logger.warning(f"服务器 {server_name} 缺少URL配置")
120
+ continue
121
+
122
+ server_configs[server_name] = {
123
+ 'url': url,
124
+ 'transport': transport
125
+ }
126
+
127
+ if enabled_tools:
128
+ self._enabled_tools_map[server_name] = enabled_tools
129
+ logger.info(f"服务器 {server_name} 启用工具: {enabled_tools}")
130
+ else:
131
+ logger.info(f"服务器 {server_name} 将使用所有可用工具")
132
+
133
+ if not server_configs:
134
+ logger.warning("没有有效的MCP服务器配置")
135
+ return
136
+
137
+ self._client = MultiServerMCPClient(server_configs)
138
+
139
+ self._initialized = True
140
+
141
+ tools = await self.get_all_tools()
142
+ logger.info(f"成功初始化MCP客户端,加载了 {len(tools)} 个工具")
143
+
144
+ except Exception as e:
145
+ logger.error(f"MCP客户端初始化失败: {str(e)}", exc_info=True)
146
+ raise MCPToolsNotAvailableError(f"初始化失败: {str(e)}") from e
147
+
148
+ async def get_all_tools(self) -> List[BaseTool]:
149
+ """
150
+ 获取所有MCP工具
151
+
152
+ 如果配置了 enabled_tools,只返回启用的工具;
153
+ 否则返回所有可用工具。
154
+
155
+ Returns:
156
+ List[BaseTool]: 所有可用工具的列表(LangChain工具)
157
+ """
158
+ if not self._initialized:
159
+ await self.initialize()
160
+
161
+ if not self._client:
162
+ logger.warning("MCP客户端未初始化")
163
+ return []
164
+
165
+ try:
166
+ all_tools = await self._client.get_tools()
167
+
168
+ if not self._enabled_tools_map:
169
+ logger.debug(f"获取到 {len(all_tools)} 个MCP工具(使用所有工具)")
170
+ return all_tools
171
+
172
+ enabled_tools = []
173
+ all_enabled_tool_names = set()
174
+
175
+ for server_name, tool_names in self._enabled_tools_map.items():
176
+ all_enabled_tool_names.update(tool_names)
177
+
178
+ for tool in all_tools:
179
+ if tool.name in all_enabled_tool_names:
180
+ enabled_tools.append(tool)
181
+
182
+ logger.info(f"获取到 {len(all_tools)} 个MCP工具,启用了 {len(enabled_tools)} 个工具")
183
+ if len(enabled_tools) < len(all_enabled_tool_names):
184
+ missing_tools = all_enabled_tool_names - {t.name for t in enabled_tools}
185
+ logger.warning(f"配置中的某些工具未找到: {missing_tools}")
186
+
187
+ return enabled_tools
188
+
189
+ except Exception as e:
190
+ logger.error(f"获取MCP工具失败: {str(e)}", exc_info=True)
191
+ return []
192
+
193
+ async def get_tool_by_name(self, tool_name: str) -> Optional[BaseTool]:
194
+ """
195
+ 根据名称获取特定工具
196
+
197
+ Args:
198
+ tool_name: 工具名称
199
+
200
+ Returns:
201
+ 工具实例,如果未找到返回None
202
+ """
203
+ tools = await self.get_all_tools()
204
+ for tool in tools:
205
+ if tool.name == tool_name:
206
+ return tool
207
+ return None
208
+
209
+ async def get_tools_by_server(self, server_names: str | List[str]) -> List[BaseTool]:
210
+ """
211
+ 根据服务器名称获取工具(支持单个或多个服务器)
212
+
213
+ Args:
214
+ server_names: 服务器名称或服务器名称列表
215
+
216
+ Returns:
217
+ List[BaseTool]: 工具列表(如果是多个服务器,会合并去重)
218
+
219
+ Examples:
220
+ # 获取单个服务器的工具
221
+ tools = await mcp_client.get_tools_by_server("web_search_server")
222
+
223
+ # 获取多个服务器的工具
224
+ tools = await mcp_client.get_tools_by_server(["web_search_server", "file-processor"])
225
+
226
+ # 也支持传入逗号分隔的字符串
227
+ tools = await mcp_client.get_tools_by_server("web_search_server,file-processor")
228
+ """
229
+ if not self._initialized:
230
+ await self.initialize()
231
+
232
+ if not self._client:
233
+ logger.warning("MCP客户端未初始化")
234
+ return []
235
+
236
+ # 统一处理输入格式
237
+ if isinstance(server_names, str):
238
+ # 支持逗号分隔的字符串
239
+ if ',' in server_names:
240
+ server_list = [s.strip() for s in server_names.split(',') if s.strip()]
241
+ else:
242
+ server_list = [server_names]
243
+ else:
244
+ server_list = server_names
245
+
246
+ if not server_list:
247
+ logger.warning("未提供有效的服务器名称")
248
+ return []
249
+
250
+ # 收集所有工具(去重)
251
+ all_server_tools = []
252
+ seen_tool_names = set()
253
+
254
+ for server_name in server_list:
255
+ # 检查缓存
256
+ if server_name in self._server_tools_cache:
257
+ logger.debug(f"使用缓存的服务器工具: {server_name}")
258
+ server_tools = self._server_tools_cache[server_name]
259
+ else:
260
+ # 获取该服务器的工具
261
+ server_tools = await self._get_single_server_tools(server_name)
262
+ # 缓存结果
263
+ self._server_tools_cache[server_name] = server_tools
264
+
265
+ # 添加到结果中(去重)
266
+ for tool in server_tools:
267
+ if tool.name not in seen_tool_names:
268
+ all_server_tools.append(tool)
269
+ seen_tool_names.add(tool.name)
270
+
271
+ if len(server_list) == 1:
272
+ logger.info(f"服务器 {server_list[0]} 有 {len(all_server_tools)} 个工具")
273
+ else:
274
+ logger.info(f"从 {len(server_list)} 个服务器获取了 {len(all_server_tools)} 个工具(已去重)")
275
+
276
+ return all_server_tools
277
+
278
+ async def _get_single_server_tools(self, server_name: str) -> List[BaseTool]:
279
+ """
280
+ 获取单个服务器的工具(内部方法)
281
+
282
+ Args:
283
+ server_name: 服务器名称
284
+
285
+ Returns:
286
+ List[BaseTool]: 该服务器的工具列表
287
+ """
288
+ try:
289
+ all_tools = await self._client.get_tools()
290
+ server_tools = []
291
+
292
+ for tool in all_tools:
293
+ # 方式1: 检查 tool.server 属性
294
+ if hasattr(tool, 'server') and tool.server == server_name:
295
+ server_tools.append(tool)
296
+ # 方式2: 检查 tool.metadata
297
+ elif hasattr(tool, 'metadata') and tool.metadata is not None and tool.metadata.get('server') == server_name:
298
+ server_tools.append(tool)
299
+ # 方式3: 检查 enabled_tools_map 配置
300
+ elif server_name in self._enabled_tools_map:
301
+ if tool.name in self._enabled_tools_map[server_name]:
302
+ server_tools.append(tool)
303
+
304
+ # 如果前面的方式都没找到,但配置了 enabled_tools,尝试按工具名匹配
305
+ if not server_tools and server_name in self._enabled_tools_map:
306
+ enabled_tool_names = self._enabled_tools_map[server_name]
307
+ server_tools = [
308
+ tool for tool in all_tools
309
+ if tool.name in enabled_tool_names
310
+ ]
311
+
312
+ logger.debug(f"服务器 {server_name} 有 {len(server_tools)} 个工具")
313
+ return server_tools
314
+
315
+ except Exception as e:
316
+ logger.error(f"获取服务器 {server_name} 的工具失败: {str(e)}", exc_info=True)
317
+ return []
318
+
319
+ async def list_servers(self) -> List[str]:
320
+ """
321
+ 列出所有配置的服务器名称
322
+
323
+ Returns:
324
+ List[str]: 服务器名称列表
325
+
326
+ Examples:
327
+ servers = await mcp_client.list_servers()
328
+ print(f"已配置的服务器: {servers}")
329
+ """
330
+ mcp_config = self._get_mcp_config()
331
+ default_servers = mcp_config.get('default_servers', {})
332
+ return list(default_servers.keys())
333
+
334
+ def close(self):
335
+ """
336
+ 清理 MCP 客户端
337
+ """
338
+ if self._client and self._initialized:
339
+ self._initialized = False
340
+ self._client = None
341
+ self._server_tools_cache.clear()
342
+ logger.info("MCP客户端已清理")
343
+
344
+ # mcp_client = MCPClient()
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
2
  import os
3
3
  from contextlib import contextmanager
4
- from dotenv import load_dotenv
4
+ from dotenv import load_dotenv, find_dotenv
5
5
  from sqlalchemy import create_engine, text
6
6
  from sqlalchemy.exc import OperationalError
7
7
  from sqlalchemy.orm import sessionmaker, scoped_session
8
8
 
9
- load_dotenv()
9
+ load_dotenv(find_dotenv(usecwd=True))
10
10
 
11
11
 
12
12
  sqlalchemy_database_uri = os.getenv("SQLALCHEMY_DATABASE_URI", "sqlite:///fallback.db")
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import Dict
6
+
7
+ from redis.asyncio import ConnectionPool, Redis
8
+
9
+
10
+ class RedisPoolManager:
11
+ """
12
+ 单例,负责:
13
+ 1. 持有全局 ConnectionPool
14
+ 2. 记录 client_id -> Redis 实例
15
+ 3. 生命周期内统一创建/销毁
16
+ """
17
+ _inst: RedisPoolManager | None = None
18
+ _lock = asyncio.Lock()
19
+
20
+ def __new__(cls, *a, **kw):
21
+ if cls._inst is None:
22
+ cls._inst = super().__new__(cls)
23
+ return cls._inst
24
+
25
+ def __init__(
26
+ self,
27
+ redis_url: str = "redis://localhost:6379/0",
28
+ max_connections: int = 256,
29
+ ):
30
+ # 只允许初始化一次
31
+ if hasattr(self, "_init"):
32
+ return
33
+ self.redis_url = redis_url
34
+ self.max_connections = max_connections
35
+ self._pool: ConnectionPool | None = None
36
+ self._clients: Dict[str, Redis] = {}
37
+ self._init = True
38
+
39
+ async def ensure_pool(self):
40
+ """延迟创建pool"""
41
+ if self._pool is None:
42
+ self._pool = ConnectionPool.from_url(
43
+ self.redis_url,
44
+ max_connections=self.max_connections,
45
+ decode_responses=True, # 直接 str,免手动 decode
46
+ )
47
+
48
+ async def startup(self):
49
+ """FastAPI startup 事件回调"""
50
+ await self.ensure_pool()
51
+
52
+ async def shutdown(self):
53
+ """FastAPI shutdown 事件回调"""
54
+ async with self._lock:
55
+ # 1. 关闭所有客户端
56
+ await asyncio.gather(*(c.aclose() for c in self._clients.values()))
57
+
58
+ self._clients.clear()
59
+ # 2. 关闭连接池
60
+ if self._pool:
61
+ await self._pool.aclose()
62
+ self._pool = None
63
+
64
+ async def get_client(self, client_id: str) -> Redis:
65
+ """线程安全地获取(或创建)一个 Redis 实例,复用同一池"""
66
+ async with self._lock:
67
+ if client_id in self._clients:
68
+ return self._clients[client_id]
69
+ await self.ensure_pool()
70
+ redis = Redis.from_pool(connection_pool=self._pool)
71
+ self._clients[client_id] = redis
72
+ return redis
73
+
74
+ async def close_client(self, client_id: str) -> bool:
75
+ """线程安全地关闭一个 Redis 实例"""
76
+ async with self._lock:
77
+ if client_id in self._clients:
78
+ await self._clients[client_id].aclose()
79
+ await self._clients.pop(client_id)
80
+ return True
81
+ return False
82
+
83
+ async def list_clients(self) -> None:
84
+ """线程安全地关闭一个 Redis 实例"""
85
+ async with self._lock:
86
+ clients = [c for c in self._clients.values()]
87
+ print(f'共{len(clients)}个client')
88
+
89
+ async def __aenter__(self):
90
+ """异步上下文管理器入口"""
91
+ await self.startup()
92
+ return self
93
+
94
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
95
+ """异步上下文管理器出口"""
96
+ await self.shutdown()
97
+
98
+
99
+ if __name__ == '__main__':
100
+ global_redis_pool_manager = RedisPoolManager()
101
+
102
+ async def _main():
103
+ client = await global_redis_pool_manager.get_client(client_id='test_1234')
104
+ await global_redis_pool_manager.list_clients()
105
+
106
+ await client.set('k1', 'hello', ex=3) # 设值 + 30 秒过期
107
+ val = await client.get('k1')
108
+ print(f'get redis cache: {val}')
109
+
110
+ time.sleep(3)
111
+
112
+ val = await client.get('k1')
113
+ print(f'get redis cache: {val}')
114
+
115
+ await global_redis_pool_manager.close_client(client_id='test_1234')
116
+ await global_redis_pool_manager.list_clients()
117
+
118
+ asyncio.run(_main())
@@ -1,11 +1,11 @@
1
1
  from aliyunsdkcore.client import AcsClient
2
2
  from aliyunsdkcore.request import CommonRequest
3
- from dotenv import load_dotenv
3
+ from dotenv import load_dotenv, find_dotenv
4
4
  from pydantic import Field
5
5
  from pydantic_settings import BaseSettings
6
6
 
7
7
 
8
- load_dotenv()
8
+ load_dotenv(find_dotenv(usecwd=True))
9
9
 
10
10
 
11
11
  class SmsSettings(BaseSettings):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-kit-common
3
- Version: 0.0.17
3
+ Version: 0.0.21
4
4
  Summary: Common utilities and tools for agentic kit ecosystem
5
5
  Home-page:
6
6
  Author: manson
@@ -25,12 +25,14 @@ Requires-Dist: langgraph
25
25
  Requires-Dist: langchain_community
26
26
  Requires-Dist: langchain_experimental
27
27
  Requires-Dist: langchain-openai
28
+ Requires-Dist: langchain_mcp_adapters
28
29
  Requires-Dist: mysql-connector-python
29
30
  Requires-Dist: sqlalchemy
30
31
  Requires-Dist: sqlglot
31
32
  Requires-Dist: pymilvus
32
33
  Requires-Dist: xinference_client
33
34
  Requires-Dist: pymongo
35
+ Requires-Dist: redis
34
36
  Requires-Dist: aliyun-python-sdk-core
35
37
  Requires-Dist: aliyun-python-sdk-dysmsapi
36
38
  Dynamic: author
@@ -6,11 +6,15 @@ agentic_kit_common.egg-info/SOURCES.txt
6
6
  agentic_kit_common.egg-info/dependency_links.txt
7
7
  agentic_kit_common.egg-info/requires.txt
8
8
  agentic_kit_common.egg-info/top_level.txt
9
+ agentic_kit_common/config/__init__.py
10
+ agentic_kit_common/config/config_loader.py
9
11
  agentic_kit_common/llm/__init__.py
10
12
  agentic_kit_common/llm/openai.py
11
13
  agentic_kit_common/llm/utils.py
12
14
  agentic_kit_common/log/__init__.py
13
15
  agentic_kit_common/log/logger.py
16
+ agentic_kit_common/mcp/__init__.py
17
+ agentic_kit_common/mcp/mcp_client.py
14
18
  agentic_kit_common/minio/__init__.py
15
19
  agentic_kit_common/minio/minio_manager.py
16
20
  agentic_kit_common/mongodb/__init__.py
@@ -22,6 +26,8 @@ agentic_kit_common/orm/manager.py
22
26
  agentic_kit_common/orm/multi_session.py
23
27
  agentic_kit_common/orm/schema.py
24
28
  agentic_kit_common/orm/session.py
29
+ agentic_kit_common/redis/__init__.py
30
+ agentic_kit_common/redis/redis_pool_manager.py
25
31
  agentic_kit_common/sms/__init__.py
26
32
  agentic_kit_common/sms/ali_sms_client.py
27
33
  agentic_kit_common/vector/__init__.py
@@ -36,9 +42,12 @@ agentic_kit_common/web/__init__.py
36
42
  agentic_kit_common/web/http/__init__.py
37
43
  agentic_kit_common/web/http/response.py
38
44
  test/__init__.py
45
+ test/conf.yaml
39
46
  test/config.py
40
47
  test/settings.py
48
+ test/test_config_manager.py
41
49
  test/test_embedding.py
50
+ test/test_mcp_client.py
42
51
  test/test_minio.py
43
52
  test/test_orm.py
44
53
  test/test_vector.py
@@ -7,11 +7,13 @@ langgraph
7
7
  langchain_community
8
8
  langchain_experimental
9
9
  langchain-openai
10
+ langchain_mcp_adapters
10
11
  mysql-connector-python
11
12
  sqlalchemy
12
13
  sqlglot
13
14
  pymilvus
14
15
  xinference_client
15
16
  pymongo
17
+ redis
16
18
  aliyun-python-sdk-core
17
19
  aliyun-python-sdk-dysmsapi
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
 
5
5
  setup(
6
6
  name='agentic-kit-common',
7
- version="0.0.17",
7
+ version="0.0.21",
8
8
  author="manson",
9
9
  author_email="manson.li3307@gmail.com",
10
10
  description='Common utilities and tools for agentic kit ecosystem',
@@ -38,6 +38,7 @@ setup(
38
38
  "langchain_community",
39
39
  "langchain_experimental",
40
40
  "langchain-openai",
41
+ "langchain_mcp_adapters",
41
42
 
42
43
  # sqlalchemy
43
44
  "mysql-connector-python",
@@ -55,6 +56,9 @@ setup(
55
56
  # mongodb
56
57
  "pymongo",
57
58
 
59
+ # redis
60
+ "redis",
61
+
58
62
  # aliyun sms
59
63
  "aliyun-python-sdk-core",
60
64
  "aliyun-python-sdk-dysmsapi",
File without changes
@@ -0,0 +1,56 @@
1
+ BASIC_MODEL:
2
+ default: qwen3-235b-a22b
3
+ models:
4
+ qwen3-235b-a22b:
5
+ base_url: http://45.120.102.120:8990/v1
6
+ model: /mnt/data0/hf_models/models/Qwen/Qwen3-235B-A22B-Instruct-2507-FP8
7
+ api_key: YOUR_API_KEY
8
+ temperature: 0.1
9
+
10
+ VISION_MODEL:
11
+ default: qwen3-vl-235b-instruct
12
+ models:
13
+ qwen3-vl-235b-instruct:
14
+ base_url: http://45.120.102.120:10021/v1
15
+ model: Qwen3-VL-235B-A22B-Instruct-FP8
16
+ api_key: YOUR_API_KEY
17
+ temperature: 0.1
18
+
19
+ llm_ocr:
20
+ base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
21
+ model: qwen-vl-ocr-latest
22
+ api_key: sk-c254cefb0b60414aa9a9686459c05602
23
+ temperature: 0.1
24
+
25
+
26
+ # RAG 客户端配置
27
+ RAG_CLIENT:
28
+ base_url: http://45.120.102.233:8900
29
+ timeout: 120
30
+
31
+ # NL2SQL 数据库查询配置
32
+ DATABASE_CLIENT:
33
+ base_url: http://45.120.102.233:8991
34
+ timeout: 30
35
+
36
+
37
+ # MCP 服务器配置
38
+ MCP_SETTINGS:
39
+ default_servers:
40
+ file-processor:
41
+ transport: streamable_http
42
+ url: http://45.120.102.231:8920/mcp
43
+ enabled_tools:
44
+ - convert_to_markdown
45
+
46
+ web_search_server:
47
+ transport: streamable_http
48
+ url: http://45.120.102.231:8910/mcp
49
+ enabled_tools:
50
+ - web_search
51
+
52
+ crawl_page_server:
53
+ transport: streamable_http
54
+ url: http://45.120.102.231:8926/mcp
55
+ enabled_tools:
56
+ - crawl_page
@@ -0,0 +1,29 @@
1
+ import logging
2
+ import sys
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+
7
+ project_root = Path(__file__).parent.parent
8
+ sys.path.insert(0, str(project_root))
9
+
10
+ logging.basicConfig(level=logging.DEBUG)
11
+
12
+ from agentic_kit_common.config import ConfigManager
13
+
14
+
15
+ class MyTestCase(unittest.TestCase):
16
+ def test_config_manager(self):
17
+ config_manager = ConfigManager()
18
+ config_manager.load_config(force_reload=True)
19
+
20
+ print(config_manager.get_llm_config())
21
+ print(config_manager.get_llm_config(model_type='VISION_MODEL'))
22
+ print(config_manager.get_rag_config())
23
+ print(config_manager.get_mcp_config())
24
+ print(config_manager.get_database_config())
25
+ print(config_manager.get_full_config())
26
+
27
+
28
+ if __name__ == '__main__':
29
+ unittest.main()
@@ -0,0 +1,29 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ from agentic_kit_common.mcp.mcp_client import MCPClient
8
+
9
+ project_root = Path(__file__).parent.parent
10
+ sys.path.insert(0, str(project_root))
11
+
12
+ logging.basicConfig(level=logging.DEBUG)
13
+
14
+ from agentic_kit_common.config import ConfigManager
15
+
16
+
17
+ class MyTestCase(unittest.TestCase):
18
+ def test_mcp_client(self):
19
+ config_manager = ConfigManager()
20
+ config_manager.load_config(force_reload=True)
21
+
22
+ mcp_client = MCPClient(config_manager=config_manager)
23
+ mcp_client.initialize()
24
+ print(asyncio.run(mcp_client.list_servers()))
25
+ print(asyncio.run(mcp_client.get_all_tools()))
26
+
27
+
28
+ if __name__ == '__main__':
29
+ unittest.main()