maque 0.2.1__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.
- maque/__init__.py +30 -0
- maque/__main__.py +926 -0
- maque/ai_platform/__init__.py +0 -0
- maque/ai_platform/crawl.py +45 -0
- maque/ai_platform/metrics.py +258 -0
- maque/ai_platform/nlp_preprocess.py +67 -0
- maque/ai_platform/webpage_screen_shot.py +195 -0
- maque/algorithms/__init__.py +78 -0
- maque/algorithms/bezier.py +15 -0
- maque/algorithms/bktree.py +117 -0
- maque/algorithms/core.py +104 -0
- maque/algorithms/hilbert.py +16 -0
- maque/algorithms/rate_function.py +92 -0
- maque/algorithms/transform.py +27 -0
- maque/algorithms/trie.py +272 -0
- maque/algorithms/utils.py +63 -0
- maque/algorithms/video.py +587 -0
- maque/api/__init__.py +1 -0
- maque/api/common.py +110 -0
- maque/api/fetch.py +26 -0
- maque/api/static/icon.png +0 -0
- maque/api/static/redoc.standalone.js +1782 -0
- maque/api/static/swagger-ui-bundle.js +3 -0
- maque/api/static/swagger-ui.css +3 -0
- maque/cli/__init__.py +1 -0
- maque/cli/clean_invisible_chars.py +324 -0
- maque/cli/core.py +34 -0
- maque/cli/groups/__init__.py +26 -0
- maque/cli/groups/config.py +205 -0
- maque/cli/groups/data.py +615 -0
- maque/cli/groups/doctor.py +259 -0
- maque/cli/groups/embedding.py +222 -0
- maque/cli/groups/git.py +29 -0
- maque/cli/groups/help.py +410 -0
- maque/cli/groups/llm.py +223 -0
- maque/cli/groups/mcp.py +241 -0
- maque/cli/groups/mllm.py +1795 -0
- maque/cli/groups/mllm_simple.py +60 -0
- maque/cli/groups/quant.py +210 -0
- maque/cli/groups/service.py +490 -0
- maque/cli/groups/system.py +570 -0
- maque/cli/mllm_run.py +1451 -0
- maque/cli/script.py +52 -0
- maque/cli/tree.py +49 -0
- maque/clustering/__init__.py +52 -0
- maque/clustering/analyzer.py +347 -0
- maque/clustering/clusterers.py +464 -0
- maque/clustering/sampler.py +134 -0
- maque/clustering/visualizer.py +205 -0
- maque/constant.py +13 -0
- maque/core.py +133 -0
- maque/cv/__init__.py +1 -0
- maque/cv/image.py +219 -0
- maque/cv/utils.py +68 -0
- maque/cv/video/__init__.py +3 -0
- maque/cv/video/keyframe_extractor.py +368 -0
- maque/embedding/__init__.py +43 -0
- maque/embedding/base.py +56 -0
- maque/embedding/multimodal.py +308 -0
- maque/embedding/server.py +523 -0
- maque/embedding/text.py +311 -0
- maque/git/__init__.py +24 -0
- maque/git/pure_git.py +912 -0
- maque/io/__init__.py +29 -0
- maque/io/core.py +38 -0
- maque/io/ops.py +194 -0
- maque/llm/__init__.py +111 -0
- maque/llm/backend.py +416 -0
- maque/llm/base.py +411 -0
- maque/llm/server.py +366 -0
- maque/mcp_server.py +1096 -0
- maque/mllm_data_processor_pipeline/__init__.py +17 -0
- maque/mllm_data_processor_pipeline/core.py +341 -0
- maque/mllm_data_processor_pipeline/example.py +291 -0
- maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
- maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
- maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
- maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
- maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
- maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
- maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
- maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
- maque/mllm_data_processor_pipeline/web_app.py +317 -0
- maque/nlp/__init__.py +14 -0
- maque/nlp/ngram.py +9 -0
- maque/nlp/parser.py +63 -0
- maque/nlp/risk_matcher.py +543 -0
- maque/nlp/sentence_splitter.py +202 -0
- maque/nlp/simple_tradition_cvt.py +31 -0
- maque/performance/__init__.py +21 -0
- maque/performance/_measure_time.py +70 -0
- maque/performance/_profiler.py +367 -0
- maque/performance/_stat_memory.py +51 -0
- maque/pipelines/__init__.py +15 -0
- maque/pipelines/clustering.py +252 -0
- maque/quantization/__init__.py +42 -0
- maque/quantization/auto_round.py +120 -0
- maque/quantization/base.py +145 -0
- maque/quantization/bitsandbytes.py +127 -0
- maque/quantization/llm_compressor.py +102 -0
- maque/retriever/__init__.py +35 -0
- maque/retriever/chroma.py +654 -0
- maque/retriever/document.py +140 -0
- maque/retriever/milvus.py +1140 -0
- maque/table_ops/__init__.py +1 -0
- maque/table_ops/core.py +133 -0
- maque/table_viewer/__init__.py +4 -0
- maque/table_viewer/download_assets.py +57 -0
- maque/table_viewer/server.py +698 -0
- maque/table_viewer/static/element-plus-icons.js +5791 -0
- maque/table_viewer/static/element-plus.css +1 -0
- maque/table_viewer/static/element-plus.js +65236 -0
- maque/table_viewer/static/main.css +268 -0
- maque/table_viewer/static/main.js +669 -0
- maque/table_viewer/static/vue.global.js +18227 -0
- maque/table_viewer/templates/index.html +401 -0
- maque/utils/__init__.py +56 -0
- maque/utils/color.py +68 -0
- maque/utils/color_string.py +45 -0
- maque/utils/compress.py +66 -0
- maque/utils/constant.py +183 -0
- maque/utils/core.py +261 -0
- maque/utils/cursor.py +143 -0
- maque/utils/distance.py +58 -0
- maque/utils/docker.py +96 -0
- maque/utils/downloads.py +51 -0
- maque/utils/excel_helper.py +542 -0
- maque/utils/helper_metrics.py +121 -0
- maque/utils/helper_parser.py +168 -0
- maque/utils/net.py +64 -0
- maque/utils/nvidia_stat.py +140 -0
- maque/utils/ops.py +53 -0
- maque/utils/packages.py +31 -0
- maque/utils/path.py +57 -0
- maque/utils/tar.py +260 -0
- maque/utils/untar.py +129 -0
- maque/web/__init__.py +0 -0
- maque/web/image_downloader.py +1410 -0
- maque-0.2.1.dist-info/METADATA +450 -0
- maque-0.2.1.dist-info/RECORD +143 -0
- maque-0.2.1.dist-info/WHEEL +4 -0
- maque-0.2.1.dist-info/entry_points.txt +3 -0
- maque-0.2.1.dist-info/licenses/LICENSE +21 -0
maque/mcp_server.py
ADDED
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
"""
|
|
2
|
+
maque MCP Server - API 文档查询 + LLM 调用服务
|
|
3
|
+
|
|
4
|
+
提供两大功能:
|
|
5
|
+
1. API 文档查询:让 AI Agent 查询 maque 的可用功能
|
|
6
|
+
2. LLM 调用:直接调用配置好的 LLM 进行推理
|
|
7
|
+
|
|
8
|
+
启动方式:
|
|
9
|
+
python -m maque.mcp_server
|
|
10
|
+
|
|
11
|
+
配置 Claude Code (~/.claude.json):
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"maque": {
|
|
15
|
+
"command": "python",
|
|
16
|
+
"args": ["-m", "maque.mcp_server"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
import asyncio
|
|
24
|
+
import inspect
|
|
25
|
+
import pkgutil
|
|
26
|
+
import importlib
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional, List, Dict, Any
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
from mcp.server import Server
|
|
32
|
+
from mcp.server.stdio import stdio_server
|
|
33
|
+
from mcp.types import TextContent, Tool
|
|
34
|
+
|
|
35
|
+
# 核心模块列表(按重要性排序)- 只保留模块名,其他信息自动提取
|
|
36
|
+
CORE_MODULES = [
|
|
37
|
+
"mllm", # LLM/MLLM 客户端
|
|
38
|
+
"embedding", # 文本/多模态 Embedding
|
|
39
|
+
"async_api", # 异步并发执行
|
|
40
|
+
"retriever", # RAG 检索
|
|
41
|
+
"clustering", # 聚类分析
|
|
42
|
+
"io", # 文件 IO
|
|
43
|
+
"performance",# 性能监控
|
|
44
|
+
"llm", # LLM 推理服务
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_module_exports(module_name: str) -> list[str]:
|
|
49
|
+
"""从模块的 __init__.py 自动获取 __all__ 导出列表"""
|
|
50
|
+
root = get_maque_root()
|
|
51
|
+
init_file = root / module_name / "__init__.py"
|
|
52
|
+
|
|
53
|
+
if not init_file.exists():
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
source = init_file.read_text(encoding='utf-8')
|
|
58
|
+
tree = ast.parse(source)
|
|
59
|
+
|
|
60
|
+
for node in ast.walk(tree):
|
|
61
|
+
if isinstance(node, ast.Assign):
|
|
62
|
+
for target in node.targets:
|
|
63
|
+
if isinstance(target, ast.Name) and target.id == "__all__":
|
|
64
|
+
if isinstance(node.value, ast.List):
|
|
65
|
+
return [
|
|
66
|
+
elt.value for elt in node.value.elts
|
|
67
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str)
|
|
68
|
+
]
|
|
69
|
+
return []
|
|
70
|
+
except Exception:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_module_docstring(module_name: str) -> tuple[str, str]:
|
|
75
|
+
"""
|
|
76
|
+
从模块的 __init__.py 自动获取 docstring
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
(description, example): 描述和示例代码
|
|
80
|
+
"""
|
|
81
|
+
root = get_maque_root()
|
|
82
|
+
init_file = root / module_name / "__init__.py"
|
|
83
|
+
|
|
84
|
+
if not init_file.exists():
|
|
85
|
+
return ("", "")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
source = init_file.read_text(encoding='utf-8')
|
|
89
|
+
tree = ast.parse(source)
|
|
90
|
+
docstring = ast.get_docstring(tree) or ""
|
|
91
|
+
|
|
92
|
+
# 分离描述和示例
|
|
93
|
+
if "Example:" in docstring:
|
|
94
|
+
parts = docstring.split("Example:", 1)
|
|
95
|
+
description = parts[0].strip()
|
|
96
|
+
example = parts[1].strip() if len(parts) > 1 else ""
|
|
97
|
+
else:
|
|
98
|
+
description = docstring.split("\n\n")[0].strip() # 取第一段作为描述
|
|
99
|
+
example = ""
|
|
100
|
+
|
|
101
|
+
return (description, example)
|
|
102
|
+
except Exception:
|
|
103
|
+
return ("", "")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class APIInfo:
|
|
108
|
+
"""API 信息"""
|
|
109
|
+
name: str
|
|
110
|
+
module: str
|
|
111
|
+
type: str # 'class' | 'function' | 'module'
|
|
112
|
+
signature: str
|
|
113
|
+
docstring: str
|
|
114
|
+
example: str = ""
|
|
115
|
+
methods: list = None # 类的主要方法列表
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_maque_root() -> Path:
|
|
119
|
+
"""获取 maque 包的根目录"""
|
|
120
|
+
import maque
|
|
121
|
+
return Path(maque.__file__).parent
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def extract_docstring_from_source(file_path: Path, target_name: str) -> Optional[str]:
|
|
125
|
+
"""从源码中提取指定类或函数的 docstring(不导入模块)"""
|
|
126
|
+
try:
|
|
127
|
+
source = file_path.read_text(encoding='utf-8')
|
|
128
|
+
tree = ast.parse(source)
|
|
129
|
+
|
|
130
|
+
for node in ast.walk(tree):
|
|
131
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
132
|
+
if node.name == target_name:
|
|
133
|
+
return ast.get_docstring(node)
|
|
134
|
+
return None
|
|
135
|
+
except Exception:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def extract_class_info_from_source(file_path: Path, class_name: str) -> Optional[APIInfo]:
|
|
140
|
+
"""从源码提取类信息(包括方法列表)"""
|
|
141
|
+
try:
|
|
142
|
+
source = file_path.read_text(encoding='utf-8')
|
|
143
|
+
tree = ast.parse(source)
|
|
144
|
+
|
|
145
|
+
for node in ast.walk(tree):
|
|
146
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
147
|
+
docstring = ast.get_docstring(node) or "无文档"
|
|
148
|
+
|
|
149
|
+
# 提取 __init__ 签名
|
|
150
|
+
init_sig = ""
|
|
151
|
+
methods = []
|
|
152
|
+
for item in node.body:
|
|
153
|
+
if isinstance(item, ast.FunctionDef) and item.name == "__init__":
|
|
154
|
+
args = []
|
|
155
|
+
for arg in item.args.args[1:]: # 跳过 self
|
|
156
|
+
arg_str = arg.arg
|
|
157
|
+
if arg.annotation:
|
|
158
|
+
arg_str += f": {ast.unparse(arg.annotation)}"
|
|
159
|
+
args.append(arg_str)
|
|
160
|
+
|
|
161
|
+
# 处理默认值
|
|
162
|
+
defaults = item.args.defaults
|
|
163
|
+
num_defaults = len(defaults)
|
|
164
|
+
num_args = len(args)
|
|
165
|
+
for i, default in enumerate(defaults):
|
|
166
|
+
arg_idx = num_args - num_defaults + i
|
|
167
|
+
try:
|
|
168
|
+
default_val = ast.unparse(default)
|
|
169
|
+
args[arg_idx] += f" = {default_val}"
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
init_sig = f"({', '.join(args)})"
|
|
174
|
+
|
|
175
|
+
# 提取公开方法(不以 _ 开头,排除 __init__ 等)
|
|
176
|
+
elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
177
|
+
if not item.name.startswith('_'):
|
|
178
|
+
method_doc = ast.get_docstring(item) or ""
|
|
179
|
+
method_desc = method_doc.split('\n')[0] if method_doc else ""
|
|
180
|
+
# 构建方法签名
|
|
181
|
+
method_args = []
|
|
182
|
+
for arg in item.args.args:
|
|
183
|
+
if arg.arg in ('self', 'cls'):
|
|
184
|
+
continue
|
|
185
|
+
method_args.append(arg.arg)
|
|
186
|
+
prefix = "async " if isinstance(item, ast.AsyncFunctionDef) else ""
|
|
187
|
+
methods.append({
|
|
188
|
+
"name": item.name,
|
|
189
|
+
"signature": f"{prefix}{item.name}({', '.join(method_args)})",
|
|
190
|
+
"description": method_desc[:100] if len(method_desc) > 100 else method_desc,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return APIInfo(
|
|
194
|
+
name=class_name,
|
|
195
|
+
module=str(file_path.relative_to(get_maque_root().parent)).replace('/', '.').replace('.py', ''),
|
|
196
|
+
type='class',
|
|
197
|
+
signature=f"class {class_name}{init_sig}",
|
|
198
|
+
docstring=docstring[:1000] if len(docstring) > 1000 else docstring,
|
|
199
|
+
methods=methods[:10] if methods else None, # 最多 10 个方法
|
|
200
|
+
)
|
|
201
|
+
return None
|
|
202
|
+
except Exception as e:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def search_in_module(module_path: Path, keyword: str) -> list[APIInfo]:
|
|
207
|
+
"""在模块中搜索关键词"""
|
|
208
|
+
results = []
|
|
209
|
+
keyword_lower = keyword.lower()
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
source = module_path.read_text(encoding='utf-8')
|
|
213
|
+
tree = ast.parse(source)
|
|
214
|
+
|
|
215
|
+
for node in ast.walk(tree):
|
|
216
|
+
if isinstance(node, ast.ClassDef):
|
|
217
|
+
docstring = ast.get_docstring(node) or ""
|
|
218
|
+
if keyword_lower in node.name.lower() or keyword_lower in docstring.lower():
|
|
219
|
+
info = extract_class_info_from_source(module_path, node.name)
|
|
220
|
+
if info:
|
|
221
|
+
results.append(info)
|
|
222
|
+
|
|
223
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
224
|
+
# 跳过私有函数
|
|
225
|
+
if node.name.startswith('_'):
|
|
226
|
+
continue
|
|
227
|
+
docstring = ast.get_docstring(node) or ""
|
|
228
|
+
if keyword_lower in node.name.lower() or keyword_lower in docstring.lower():
|
|
229
|
+
# 构建签名
|
|
230
|
+
args = []
|
|
231
|
+
for arg in node.args.args:
|
|
232
|
+
arg_str = arg.arg
|
|
233
|
+
if arg.annotation:
|
|
234
|
+
try:
|
|
235
|
+
arg_str += f": {ast.unparse(arg.annotation)}"
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
args.append(arg_str)
|
|
239
|
+
|
|
240
|
+
prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
|
|
241
|
+
sig = f"{prefix} {node.name}({', '.join(args)})"
|
|
242
|
+
|
|
243
|
+
results.append(APIInfo(
|
|
244
|
+
name=node.name,
|
|
245
|
+
module=str(module_path.relative_to(get_maque_root().parent)).replace('/', '.').replace('.py', ''),
|
|
246
|
+
type='function',
|
|
247
|
+
signature=sig,
|
|
248
|
+
docstring=docstring[:300] if len(docstring) > 300 else docstring,
|
|
249
|
+
))
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
return results
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def search_maque(keyword: str) -> list[APIInfo]:
|
|
257
|
+
"""搜索 maque 中的 API"""
|
|
258
|
+
results = []
|
|
259
|
+
root = get_maque_root()
|
|
260
|
+
|
|
261
|
+
for py_file in root.rglob("*.py"):
|
|
262
|
+
# 跳过测试和私有模块
|
|
263
|
+
if '__pycache__' in str(py_file) or py_file.name.startswith('_'):
|
|
264
|
+
continue
|
|
265
|
+
results.extend(search_in_module(py_file, keyword))
|
|
266
|
+
|
|
267
|
+
return results[:20] # 限制结果数量
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_module_info(module_name: str) -> str:
|
|
271
|
+
"""获取模块的详细使用说明(自动从 docstring 提取)"""
|
|
272
|
+
description, example = get_module_docstring(module_name)
|
|
273
|
+
|
|
274
|
+
if example:
|
|
275
|
+
return example
|
|
276
|
+
|
|
277
|
+
if description:
|
|
278
|
+
return description
|
|
279
|
+
|
|
280
|
+
return f"模块 {module_name} 暂无详细使用示例。请使用 search_maque_api 搜索具体功能。"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def list_all_modules() -> str:
|
|
284
|
+
"""列出所有核心模块(自动从代码提取)"""
|
|
285
|
+
lines = ["# maque 核心模块\n"]
|
|
286
|
+
|
|
287
|
+
for module_name in CORE_MODULES:
|
|
288
|
+
description, _ = get_module_docstring(module_name)
|
|
289
|
+
exports = get_module_exports(module_name)
|
|
290
|
+
|
|
291
|
+
# 取描述的第一行
|
|
292
|
+
desc_line = description.split('\n')[0] if description else "无描述"
|
|
293
|
+
|
|
294
|
+
lines.append(f"## {module_name}")
|
|
295
|
+
lines.append(f" {desc_line}")
|
|
296
|
+
if exports:
|
|
297
|
+
# 只显示前 5 个导出
|
|
298
|
+
display_exports = exports[:5]
|
|
299
|
+
suffix = f" ... (+{len(exports)-5})" if len(exports) > 5 else ""
|
|
300
|
+
lines.append(f" 主要导出: {', '.join(display_exports)}{suffix}")
|
|
301
|
+
lines.append("")
|
|
302
|
+
|
|
303
|
+
lines.append("\n使用 `get_module_usage(module_name)` 获取详细用法")
|
|
304
|
+
return "\n".join(lines)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# CLI 命令组映射
|
|
308
|
+
CLI_GROUPS = {
|
|
309
|
+
"config": "配置管理",
|
|
310
|
+
"mllm": "多模态 LLM 操作",
|
|
311
|
+
"llm": "LLM 推理服务",
|
|
312
|
+
"data": "数据处理工具",
|
|
313
|
+
"embedding": "Embedding 服务",
|
|
314
|
+
"system": "系统工具",
|
|
315
|
+
"git": "Git 辅助命令",
|
|
316
|
+
"service": "服务管理",
|
|
317
|
+
"doctor": "诊断工具",
|
|
318
|
+
"mcp": "MCP 服务",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass
|
|
323
|
+
class CLICommand:
|
|
324
|
+
"""CLI 命令信息"""
|
|
325
|
+
name: str
|
|
326
|
+
group: str # 空字符串表示顶级命令
|
|
327
|
+
description: str
|
|
328
|
+
signature: str
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def extract_cli_commands() -> list[CLICommand]:
|
|
332
|
+
"""提取所有 CLI 命令"""
|
|
333
|
+
commands = []
|
|
334
|
+
root = get_maque_root()
|
|
335
|
+
|
|
336
|
+
# 1. 提取顶级命令(从 __main__.py 的 NewCli 类)
|
|
337
|
+
main_file = root / "__main__.py"
|
|
338
|
+
if main_file.exists():
|
|
339
|
+
commands.extend(_extract_commands_from_class(main_file, "NewCli", ""))
|
|
340
|
+
|
|
341
|
+
# 2. 提取分组命令(从 cli/groups/*.py)
|
|
342
|
+
groups_dir = root / "cli" / "groups"
|
|
343
|
+
if groups_dir.exists():
|
|
344
|
+
for py_file in groups_dir.glob("*.py"):
|
|
345
|
+
if py_file.name.startswith("_"):
|
|
346
|
+
continue
|
|
347
|
+
# 从文件名推断 group 名称
|
|
348
|
+
group_name = py_file.stem
|
|
349
|
+
if group_name == "mllm_simple":
|
|
350
|
+
continue # 跳过简化版
|
|
351
|
+
commands.extend(_extract_commands_from_file(py_file, group_name))
|
|
352
|
+
|
|
353
|
+
return commands
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _extract_commands_from_class(file_path: Path, class_name: str, group: str) -> list[CLICommand]:
|
|
357
|
+
"""从指定类中提取命令"""
|
|
358
|
+
commands = []
|
|
359
|
+
try:
|
|
360
|
+
source = file_path.read_text(encoding='utf-8')
|
|
361
|
+
tree = ast.parse(source)
|
|
362
|
+
|
|
363
|
+
for node in ast.walk(tree):
|
|
364
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
365
|
+
for item in node.body:
|
|
366
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
367
|
+
# 跳过私有方法和特殊方法
|
|
368
|
+
if item.name.startswith('_'):
|
|
369
|
+
continue
|
|
370
|
+
# 跳过属性方法(没有实际功能)
|
|
371
|
+
if any(isinstance(d, ast.Name) and d.id == 'property' for d in item.decorator_list):
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
docstring = ast.get_docstring(item) or "无描述"
|
|
375
|
+
# 只取 docstring 第一行
|
|
376
|
+
desc = docstring.split('\n')[0].strip()
|
|
377
|
+
|
|
378
|
+
# 构建签名
|
|
379
|
+
args = []
|
|
380
|
+
for arg in item.args.args:
|
|
381
|
+
if arg.arg in ('self', 'cls'):
|
|
382
|
+
continue
|
|
383
|
+
args.append(arg.arg)
|
|
384
|
+
sig = f"({', '.join(args)})" if args else "()"
|
|
385
|
+
|
|
386
|
+
commands.append(CLICommand(
|
|
387
|
+
name=item.name,
|
|
388
|
+
group=group,
|
|
389
|
+
description=desc,
|
|
390
|
+
signature=sig,
|
|
391
|
+
))
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
return commands
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _extract_commands_from_file(file_path: Path, group_name: str) -> list[CLICommand]:
|
|
398
|
+
"""从文件中提取 Group 类的命令"""
|
|
399
|
+
commands = []
|
|
400
|
+
try:
|
|
401
|
+
source = file_path.read_text(encoding='utf-8')
|
|
402
|
+
tree = ast.parse(source)
|
|
403
|
+
|
|
404
|
+
for node in ast.walk(tree):
|
|
405
|
+
if isinstance(node, ast.ClassDef) and node.name.endswith('Group'):
|
|
406
|
+
commands.extend(_extract_commands_from_class(file_path, node.name, group_name))
|
|
407
|
+
break # 每个文件只处理一个 Group 类
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
return commands
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def list_cli_commands() -> str:
|
|
414
|
+
"""列出所有 CLI 命令"""
|
|
415
|
+
commands = extract_cli_commands()
|
|
416
|
+
|
|
417
|
+
# 分组显示
|
|
418
|
+
top_level = [c for c in commands if not c.group]
|
|
419
|
+
grouped = {}
|
|
420
|
+
for c in commands:
|
|
421
|
+
if c.group:
|
|
422
|
+
if c.group not in grouped:
|
|
423
|
+
grouped[c.group] = []
|
|
424
|
+
grouped[c.group].append(c)
|
|
425
|
+
|
|
426
|
+
lines = ["# maque CLI 命令\n"]
|
|
427
|
+
|
|
428
|
+
# 顶级命令
|
|
429
|
+
if top_level:
|
|
430
|
+
lines.append("## 顶级命令")
|
|
431
|
+
lines.append("用法: `maque <command> [args]`\n")
|
|
432
|
+
for cmd in sorted(top_level, key=lambda x: x.name):
|
|
433
|
+
lines.append(f"- **{cmd.name}**{cmd.signature}: {cmd.description}")
|
|
434
|
+
lines.append("")
|
|
435
|
+
|
|
436
|
+
# 分组命令
|
|
437
|
+
lines.append("## 分组命令")
|
|
438
|
+
lines.append("用法: `maque <group> <command> [args]`\n")
|
|
439
|
+
|
|
440
|
+
for group_name in sorted(grouped.keys()):
|
|
441
|
+
group_desc = CLI_GROUPS.get(group_name, "")
|
|
442
|
+
lines.append(f"### {group_name}" + (f" - {group_desc}" if group_desc else ""))
|
|
443
|
+
for cmd in sorted(grouped[group_name], key=lambda x: x.name):
|
|
444
|
+
lines.append(f"- **{cmd.name}**{cmd.signature}: {cmd.description}")
|
|
445
|
+
lines.append("")
|
|
446
|
+
|
|
447
|
+
return "\n".join(lines)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# =============================================================================
|
|
451
|
+
# LLM-Friendly API(专为 AI Agent 优化的简化接口)
|
|
452
|
+
# =============================================================================
|
|
453
|
+
|
|
454
|
+
async def ask(question: str, context: str = None) -> str:
|
|
455
|
+
"""
|
|
456
|
+
最简单的问答接口 - 专为 LLM Agent 设计
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
question: 问题(纯文本即可)
|
|
460
|
+
context: 可选的上下文信息
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
回答文本
|
|
464
|
+
"""
|
|
465
|
+
client = _get_llm_client()
|
|
466
|
+
|
|
467
|
+
if context:
|
|
468
|
+
content = f"上下文:\n{context}\n\n问题: {question}"
|
|
469
|
+
else:
|
|
470
|
+
content = question
|
|
471
|
+
|
|
472
|
+
return await client.chat_completions(
|
|
473
|
+
messages=[{"role": "user", "content": content}]
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def ask_batch(questions: List[str]) -> List[str]:
|
|
478
|
+
"""
|
|
479
|
+
批量问答 - 接受简单的问题列表,而不是嵌套的 messages 结构
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
questions: 问题列表(简单字符串列表)
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
回答列表(与问题一一对应)
|
|
486
|
+
"""
|
|
487
|
+
client = _get_llm_client()
|
|
488
|
+
messages_list = [[{"role": "user", "content": q}] for q in questions]
|
|
489
|
+
return await client.chat_completions_batch(messages_list, show_progress=False)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
async def extract_json(text: str, schema_desc: str = None) -> Dict[str, Any]:
|
|
493
|
+
"""
|
|
494
|
+
从文本中提取 JSON 结构
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
text: 要处理的文本
|
|
498
|
+
schema_desc: 期望的 JSON 结构描述(如 "name, age, email")
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
解析后的 dict(如果解析失败则返回 {"error": "...", "raw": "..."})
|
|
502
|
+
"""
|
|
503
|
+
import json
|
|
504
|
+
import re
|
|
505
|
+
|
|
506
|
+
client = _get_llm_client()
|
|
507
|
+
|
|
508
|
+
if schema_desc:
|
|
509
|
+
prompt = f"从以下文本中提取信息,以 JSON 格式输出,包含字段: {schema_desc}\n只输出 JSON,不要其他内容:\n\n{text}"
|
|
510
|
+
else:
|
|
511
|
+
prompt = f"从以下文本中提取结构化信息,以 JSON 格式输出,不要其他内容:\n\n{text}"
|
|
512
|
+
|
|
513
|
+
result = await client.chat_completions([{"role": "user", "content": prompt}])
|
|
514
|
+
|
|
515
|
+
# 尝试解析 JSON
|
|
516
|
+
try:
|
|
517
|
+
# 尝试找到 JSON 块
|
|
518
|
+
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', result)
|
|
519
|
+
if json_match:
|
|
520
|
+
return json.loads(json_match.group(1))
|
|
521
|
+
return json.loads(result)
|
|
522
|
+
except json.JSONDecodeError:
|
|
523
|
+
return {"error": "JSON 解析失败", "raw": result}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def get_capabilities() -> Dict[str, Any]:
|
|
527
|
+
"""
|
|
528
|
+
获取当前 LLM 的能力信息 - 帮助 Agent 决策
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
模型能力字典,包含模型名、是否支持 JSON 模式等
|
|
532
|
+
"""
|
|
533
|
+
config = _load_llm_config()
|
|
534
|
+
model = config.get("model", "unknown")
|
|
535
|
+
|
|
536
|
+
# 根据模型名推断能力
|
|
537
|
+
model_lower = model.lower()
|
|
538
|
+
|
|
539
|
+
capabilities = {
|
|
540
|
+
"model": model,
|
|
541
|
+
"base_url": config.get("base_url", ""),
|
|
542
|
+
"supports_json_mode": any(x in model_lower for x in ["gpt-4", "gpt-3.5", "gemini", "qwen"]),
|
|
543
|
+
"supports_vision": any(x in model_lower for x in ["vision", "vl", "gpt-4o", "gemini"]),
|
|
544
|
+
"supports_thinking": any(x in model_lower for x in ["o1", "o3", "deepseek-r1", "gemini-2"]),
|
|
545
|
+
"max_context_estimate": 128000 if "gpt-4" in model_lower or "gemini" in model_lower else 32000,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return capabilities
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# =============================================================================
|
|
552
|
+
# LLM 客户端功能(原有接口保留)
|
|
553
|
+
# =============================================================================
|
|
554
|
+
|
|
555
|
+
def _load_llm_config() -> Dict[str, Any]:
|
|
556
|
+
"""加载 LLM 配置(从 maque 配置文件)"""
|
|
557
|
+
from maque import yaml_load
|
|
558
|
+
|
|
559
|
+
# 配置搜索路径
|
|
560
|
+
search_paths = [
|
|
561
|
+
Path.cwd() / "maque_config.yaml",
|
|
562
|
+
Path.home() / ".maque" / "config.yaml",
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
# 检查项目根目录
|
|
566
|
+
current = Path.cwd()
|
|
567
|
+
while current != current.parent:
|
|
568
|
+
if (current / ".git").exists() or (current / "pyproject.toml").exists():
|
|
569
|
+
project_config = current / "maque_config.yaml"
|
|
570
|
+
if project_config not in search_paths:
|
|
571
|
+
search_paths.insert(1, project_config)
|
|
572
|
+
break
|
|
573
|
+
current = current.parent
|
|
574
|
+
|
|
575
|
+
# 默认配置
|
|
576
|
+
default_config = {
|
|
577
|
+
"base_url": "http://localhost:11434/v1",
|
|
578
|
+
"api_key": "EMPTY",
|
|
579
|
+
"model": "gemma3:4b",
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for path in search_paths:
|
|
583
|
+
if path.exists():
|
|
584
|
+
try:
|
|
585
|
+
config = yaml_load(str(path))
|
|
586
|
+
if config and "mllm" in config:
|
|
587
|
+
mllm_config = config["mllm"]
|
|
588
|
+
return {
|
|
589
|
+
"base_url": mllm_config.get("base_url", default_config["base_url"]),
|
|
590
|
+
"api_key": mllm_config.get("api_key", default_config["api_key"]),
|
|
591
|
+
"model": mllm_config.get("model", default_config["model"]),
|
|
592
|
+
}
|
|
593
|
+
except Exception:
|
|
594
|
+
continue
|
|
595
|
+
|
|
596
|
+
return default_config
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _get_llm_client():
|
|
600
|
+
"""获取 LLMClient 实例"""
|
|
601
|
+
from flexllm import LLMClient
|
|
602
|
+
from flexllm.response_cache import ResponseCacheConfig
|
|
603
|
+
|
|
604
|
+
config = _load_llm_config()
|
|
605
|
+
return LLMClient(
|
|
606
|
+
base_url=config["base_url"],
|
|
607
|
+
api_key=config["api_key"],
|
|
608
|
+
model=config["model"],
|
|
609
|
+
cache=ResponseCacheConfig(enabled=False), # MCP 服务不需要响应缓存
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
async def llm_chat(
|
|
614
|
+
messages: List[Dict[str, str]],
|
|
615
|
+
model: str = None,
|
|
616
|
+
max_tokens: int = None,
|
|
617
|
+
temperature: float = None,
|
|
618
|
+
) -> str:
|
|
619
|
+
"""
|
|
620
|
+
调用 LLM 进行单条聊天
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
messages: 消息列表,格式为 [{"role": "user", "content": "..."}]
|
|
624
|
+
model: 模型名称(可选,使用配置默认值)
|
|
625
|
+
max_tokens: 最大生成 token 数
|
|
626
|
+
temperature: 温度参数
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
LLM 生成的回复
|
|
630
|
+
"""
|
|
631
|
+
client = _get_llm_client()
|
|
632
|
+
|
|
633
|
+
kwargs = {}
|
|
634
|
+
if max_tokens:
|
|
635
|
+
kwargs["max_tokens"] = max_tokens
|
|
636
|
+
if temperature is not None:
|
|
637
|
+
kwargs["temperature"] = temperature
|
|
638
|
+
|
|
639
|
+
result = await client.chat_completions(
|
|
640
|
+
messages=messages,
|
|
641
|
+
model=model,
|
|
642
|
+
**kwargs,
|
|
643
|
+
)
|
|
644
|
+
return result
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
async def llm_chat_batch(
|
|
648
|
+
messages_list: List[List[Dict[str, str]]],
|
|
649
|
+
model: str = None,
|
|
650
|
+
max_tokens: int = None,
|
|
651
|
+
temperature: float = None,
|
|
652
|
+
) -> List[str]:
|
|
653
|
+
"""
|
|
654
|
+
批量调用 LLM
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
messages_list: 消息列表的列表
|
|
658
|
+
model: 模型名称
|
|
659
|
+
max_tokens: 最大生成 token 数
|
|
660
|
+
temperature: 温度参数
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
LLM 生成的回复列表
|
|
664
|
+
"""
|
|
665
|
+
client = _get_llm_client()
|
|
666
|
+
|
|
667
|
+
kwargs = {}
|
|
668
|
+
if max_tokens:
|
|
669
|
+
kwargs["max_tokens"] = max_tokens
|
|
670
|
+
if temperature is not None:
|
|
671
|
+
kwargs["temperature"] = temperature
|
|
672
|
+
|
|
673
|
+
results = await client.chat_completions_batch(
|
|
674
|
+
messages_list=messages_list,
|
|
675
|
+
model=model,
|
|
676
|
+
show_progress=False,
|
|
677
|
+
**kwargs,
|
|
678
|
+
)
|
|
679
|
+
return results
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def llm_models() -> List[str]:
|
|
683
|
+
"""获取可用模型列表"""
|
|
684
|
+
client = _get_llm_client()
|
|
685
|
+
return client.model_list()
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def llm_config() -> Dict[str, Any]:
|
|
689
|
+
"""获取当前 LLM 配置"""
|
|
690
|
+
config = _load_llm_config()
|
|
691
|
+
# 隐藏 API key 的部分内容
|
|
692
|
+
api_key = config.get("api_key", "")
|
|
693
|
+
if api_key and len(api_key) > 8:
|
|
694
|
+
config["api_key"] = api_key[:4] + "****" + api_key[-4:]
|
|
695
|
+
return config
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
# 创建 MCP Server
|
|
699
|
+
server = Server("maque-docs")
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@server.list_tools()
|
|
703
|
+
async def list_tools() -> list[Tool]:
|
|
704
|
+
"""列出可用工具"""
|
|
705
|
+
return [
|
|
706
|
+
# ===== API 文档查询工具 =====
|
|
707
|
+
Tool(
|
|
708
|
+
name="search_maque_api",
|
|
709
|
+
description="搜索 maque 库中的 API(类、函数、模块)。用于查找可复用的功能,避免重复造轮子。",
|
|
710
|
+
inputSchema={
|
|
711
|
+
"type": "object",
|
|
712
|
+
"properties": {
|
|
713
|
+
"keyword": {
|
|
714
|
+
"type": "string",
|
|
715
|
+
"description": "搜索关键词,如 'embedding', 'llm', 'async', 'retry' 等"
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
"required": ["keyword"]
|
|
719
|
+
}
|
|
720
|
+
),
|
|
721
|
+
Tool(
|
|
722
|
+
name="get_module_usage",
|
|
723
|
+
description="获取 maque 指定模块的详细使用示例。",
|
|
724
|
+
inputSchema={
|
|
725
|
+
"type": "object",
|
|
726
|
+
"properties": {
|
|
727
|
+
"module": {
|
|
728
|
+
"type": "string",
|
|
729
|
+
"description": "模块名称,如 'mllm', 'embedding', 'async_api', 'io', 'retriever', 'clustering'"
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
"required": ["module"]
|
|
733
|
+
}
|
|
734
|
+
),
|
|
735
|
+
Tool(
|
|
736
|
+
name="list_maque_modules",
|
|
737
|
+
description="列出 maque 所有核心模块及其功能概述。",
|
|
738
|
+
inputSchema={
|
|
739
|
+
"type": "object",
|
|
740
|
+
"properties": {}
|
|
741
|
+
}
|
|
742
|
+
),
|
|
743
|
+
Tool(
|
|
744
|
+
name="list_cli_commands",
|
|
745
|
+
description="列出 maque 所有可用的 CLI 命令,包括顶级命令和分组命令。",
|
|
746
|
+
inputSchema={
|
|
747
|
+
"type": "object",
|
|
748
|
+
"properties": {}
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
# ===== LLM 调用工具 =====
|
|
752
|
+
Tool(
|
|
753
|
+
name="llm_chat",
|
|
754
|
+
description="调用 LLM 进行单条聊天。使用 maque 配置文件中的 LLM 设置。",
|
|
755
|
+
inputSchema={
|
|
756
|
+
"type": "object",
|
|
757
|
+
"properties": {
|
|
758
|
+
"messages": {
|
|
759
|
+
"type": "array",
|
|
760
|
+
"description": "消息列表,格式为 [{\"role\": \"user\", \"content\": \"...\"}]",
|
|
761
|
+
"items": {
|
|
762
|
+
"type": "object",
|
|
763
|
+
"properties": {
|
|
764
|
+
"role": {"type": "string", "enum": ["system", "user", "assistant"]},
|
|
765
|
+
"content": {"type": "string"}
|
|
766
|
+
},
|
|
767
|
+
"required": ["role", "content"]
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
"model": {
|
|
771
|
+
"type": "string",
|
|
772
|
+
"description": "模型名称(可选,使用配置默认值)"
|
|
773
|
+
},
|
|
774
|
+
"max_tokens": {
|
|
775
|
+
"type": "integer",
|
|
776
|
+
"description": "最大生成 token 数"
|
|
777
|
+
},
|
|
778
|
+
"temperature": {
|
|
779
|
+
"type": "number",
|
|
780
|
+
"description": "温度参数 (0-2)"
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
"required": ["messages"]
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
Tool(
|
|
787
|
+
name="llm_chat_batch",
|
|
788
|
+
description="批量调用 LLM,适合处理多个独立请求。",
|
|
789
|
+
inputSchema={
|
|
790
|
+
"type": "object",
|
|
791
|
+
"properties": {
|
|
792
|
+
"messages_list": {
|
|
793
|
+
"type": "array",
|
|
794
|
+
"description": "消息列表的列表,每个元素是一个完整的对话",
|
|
795
|
+
"items": {
|
|
796
|
+
"type": "array",
|
|
797
|
+
"items": {
|
|
798
|
+
"type": "object",
|
|
799
|
+
"properties": {
|
|
800
|
+
"role": {"type": "string"},
|
|
801
|
+
"content": {"type": "string"}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
"model": {
|
|
807
|
+
"type": "string",
|
|
808
|
+
"description": "模型名称"
|
|
809
|
+
},
|
|
810
|
+
"max_tokens": {
|
|
811
|
+
"type": "integer",
|
|
812
|
+
"description": "最大生成 token 数"
|
|
813
|
+
},
|
|
814
|
+
"temperature": {
|
|
815
|
+
"type": "number",
|
|
816
|
+
"description": "温度参数"
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
"required": ["messages_list"]
|
|
820
|
+
}
|
|
821
|
+
),
|
|
822
|
+
Tool(
|
|
823
|
+
name="llm_models",
|
|
824
|
+
description="获取可用的 LLM 模型列表。",
|
|
825
|
+
inputSchema={
|
|
826
|
+
"type": "object",
|
|
827
|
+
"properties": {}
|
|
828
|
+
}
|
|
829
|
+
),
|
|
830
|
+
Tool(
|
|
831
|
+
name="llm_config",
|
|
832
|
+
description="获取当前 LLM 配置信息(base_url, model 等)。",
|
|
833
|
+
inputSchema={
|
|
834
|
+
"type": "object",
|
|
835
|
+
"properties": {}
|
|
836
|
+
}
|
|
837
|
+
),
|
|
838
|
+
# ===== LLM-Friendly API(专为 AI Agent 优化)=====
|
|
839
|
+
Tool(
|
|
840
|
+
name="ask",
|
|
841
|
+
description="最简单的问答接口。直接传入问题字符串,无需构造 messages 数组。",
|
|
842
|
+
inputSchema={
|
|
843
|
+
"type": "object",
|
|
844
|
+
"properties": {
|
|
845
|
+
"question": {
|
|
846
|
+
"type": "string",
|
|
847
|
+
"description": "问题(纯文本)"
|
|
848
|
+
},
|
|
849
|
+
"context": {
|
|
850
|
+
"type": "string",
|
|
851
|
+
"description": "可选的上下文信息"
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
"required": ["question"]
|
|
855
|
+
}
|
|
856
|
+
),
|
|
857
|
+
Tool(
|
|
858
|
+
name="ask_batch",
|
|
859
|
+
description="批量问答。接受简单的问题列表,无需嵌套 messages 结构。",
|
|
860
|
+
inputSchema={
|
|
861
|
+
"type": "object",
|
|
862
|
+
"properties": {
|
|
863
|
+
"questions": {
|
|
864
|
+
"type": "array",
|
|
865
|
+
"items": {"type": "string"},
|
|
866
|
+
"description": "问题列表(简单字符串列表)"
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
"required": ["questions"]
|
|
870
|
+
}
|
|
871
|
+
),
|
|
872
|
+
Tool(
|
|
873
|
+
name="extract_json",
|
|
874
|
+
description="从文本中提取结构化 JSON 数据。自动解析,返回 dict 而非字符串。",
|
|
875
|
+
inputSchema={
|
|
876
|
+
"type": "object",
|
|
877
|
+
"properties": {
|
|
878
|
+
"text": {"type": "string", "description": "要处理的文本"},
|
|
879
|
+
"schema_desc": {"type": "string", "description": "期望的字段,如 'name, age, email'"}
|
|
880
|
+
},
|
|
881
|
+
"required": ["text"]
|
|
882
|
+
}
|
|
883
|
+
),
|
|
884
|
+
Tool(
|
|
885
|
+
name="get_capabilities",
|
|
886
|
+
description="获取当前 LLM 的能力信息。帮助 Agent 了解模型支持什么功能(vision, thinking 等)。",
|
|
887
|
+
inputSchema={
|
|
888
|
+
"type": "object",
|
|
889
|
+
"properties": {}
|
|
890
|
+
}
|
|
891
|
+
),
|
|
892
|
+
]
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
@server.call_tool()
|
|
896
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
897
|
+
"""处理工具调用"""
|
|
898
|
+
|
|
899
|
+
# ===== API 文档查询工具 =====
|
|
900
|
+
if name == "search_maque_api":
|
|
901
|
+
keyword = arguments.get("keyword", "")
|
|
902
|
+
results = search_maque(keyword)
|
|
903
|
+
|
|
904
|
+
if not results:
|
|
905
|
+
return [TextContent(type="text", text=f"未找到与 '{keyword}' 相关的 API")]
|
|
906
|
+
|
|
907
|
+
lines = [f"# 搜索结果: '{keyword}'\n"]
|
|
908
|
+
for api in results:
|
|
909
|
+
lines.append(f"## {api.name}")
|
|
910
|
+
lines.append(f" 模块: `{api.module}`")
|
|
911
|
+
lines.append(f" 类型: {api.type}")
|
|
912
|
+
lines.append(f" 签名: `{api.signature}`")
|
|
913
|
+
if api.docstring:
|
|
914
|
+
lines.append(f" 说明: {api.docstring}")
|
|
915
|
+
# 展示类的主要方法
|
|
916
|
+
if api.methods:
|
|
917
|
+
lines.append(f" 主要方法:")
|
|
918
|
+
for method in api.methods:
|
|
919
|
+
desc = f" - {method['description']}" if method['description'] else ""
|
|
920
|
+
lines.append(f" - `{method['signature']}`{desc}")
|
|
921
|
+
lines.append("")
|
|
922
|
+
|
|
923
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
924
|
+
|
|
925
|
+
elif name == "get_module_usage":
|
|
926
|
+
module = arguments.get("module", "")
|
|
927
|
+
usage = get_module_info(module)
|
|
928
|
+
return [TextContent(type="text", text=f"# {module} 模块使用示例\n\n```python{usage}\n```")]
|
|
929
|
+
|
|
930
|
+
elif name == "list_maque_modules":
|
|
931
|
+
return [TextContent(type="text", text=list_all_modules())]
|
|
932
|
+
|
|
933
|
+
elif name == "list_cli_commands":
|
|
934
|
+
return [TextContent(type="text", text=list_cli_commands())]
|
|
935
|
+
|
|
936
|
+
# ===== LLM 调用工具 =====
|
|
937
|
+
elif name == "llm_chat":
|
|
938
|
+
try:
|
|
939
|
+
messages = arguments.get("messages", [])
|
|
940
|
+
model = arguments.get("model")
|
|
941
|
+
max_tokens = arguments.get("max_tokens")
|
|
942
|
+
temperature = arguments.get("temperature")
|
|
943
|
+
|
|
944
|
+
result = await llm_chat(
|
|
945
|
+
messages=messages,
|
|
946
|
+
model=model,
|
|
947
|
+
max_tokens=max_tokens,
|
|
948
|
+
temperature=temperature,
|
|
949
|
+
)
|
|
950
|
+
return [TextContent(type="text", text=result)]
|
|
951
|
+
except Exception as e:
|
|
952
|
+
return [TextContent(type="text", text=f"LLM 调用失败: {str(e)}")]
|
|
953
|
+
|
|
954
|
+
elif name == "llm_chat_batch":
|
|
955
|
+
try:
|
|
956
|
+
messages_list = arguments.get("messages_list", [])
|
|
957
|
+
model = arguments.get("model")
|
|
958
|
+
max_tokens = arguments.get("max_tokens")
|
|
959
|
+
temperature = arguments.get("temperature")
|
|
960
|
+
|
|
961
|
+
results = await llm_chat_batch(
|
|
962
|
+
messages_list=messages_list,
|
|
963
|
+
model=model,
|
|
964
|
+
max_tokens=max_tokens,
|
|
965
|
+
temperature=temperature,
|
|
966
|
+
)
|
|
967
|
+
# 格式化输出
|
|
968
|
+
output_lines = ["# 批量调用结果\n"]
|
|
969
|
+
for i, result in enumerate(results, 1):
|
|
970
|
+
output_lines.append(f"## 结果 {i}")
|
|
971
|
+
output_lines.append(result)
|
|
972
|
+
output_lines.append("")
|
|
973
|
+
return [TextContent(type="text", text="\n".join(output_lines))]
|
|
974
|
+
except Exception as e:
|
|
975
|
+
return [TextContent(type="text", text=f"批量 LLM 调用失败: {str(e)}")]
|
|
976
|
+
|
|
977
|
+
elif name == "llm_models":
|
|
978
|
+
try:
|
|
979
|
+
models = llm_models()
|
|
980
|
+
if models:
|
|
981
|
+
lines = ["# 可用模型列表\n"]
|
|
982
|
+
for model in models:
|
|
983
|
+
lines.append(f"- {model}")
|
|
984
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
985
|
+
else:
|
|
986
|
+
return [TextContent(type="text", text="未获取到模型列表,请检查 LLM 服务是否正常运行")]
|
|
987
|
+
except Exception as e:
|
|
988
|
+
return [TextContent(type="text", text=f"获取模型列表失败: {str(e)}")]
|
|
989
|
+
|
|
990
|
+
elif name == "llm_config":
|
|
991
|
+
try:
|
|
992
|
+
config = llm_config()
|
|
993
|
+
lines = ["# 当前 LLM 配置\n"]
|
|
994
|
+
for key, value in config.items():
|
|
995
|
+
lines.append(f"- **{key}**: {value}")
|
|
996
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
997
|
+
except Exception as e:
|
|
998
|
+
return [TextContent(type="text", text=f"获取配置失败: {str(e)}")]
|
|
999
|
+
|
|
1000
|
+
# ===== LLM-Friendly API =====
|
|
1001
|
+
elif name == "ask":
|
|
1002
|
+
try:
|
|
1003
|
+
question = arguments.get("question", "")
|
|
1004
|
+
context = arguments.get("context")
|
|
1005
|
+
result = await ask(question, context)
|
|
1006
|
+
return [TextContent(type="text", text=result)]
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
return [TextContent(type="text", text=f"调用失败: {str(e)}")]
|
|
1009
|
+
|
|
1010
|
+
elif name == "ask_batch":
|
|
1011
|
+
try:
|
|
1012
|
+
questions = arguments.get("questions", [])
|
|
1013
|
+
results = await ask_batch(questions)
|
|
1014
|
+
output = "\n\n---\n\n".join([f"**Q{i+1}**: {q}\n**A{i+1}**: {a}" for i, (q, a) in enumerate(zip(questions, results))])
|
|
1015
|
+
return [TextContent(type="text", text=output)]
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
return [TextContent(type="text", text=f"批量调用失败: {str(e)}")]
|
|
1018
|
+
|
|
1019
|
+
elif name == "extract_json":
|
|
1020
|
+
try:
|
|
1021
|
+
import json
|
|
1022
|
+
text = arguments.get("text", "")
|
|
1023
|
+
schema_desc = arguments.get("schema_desc")
|
|
1024
|
+
result = await extract_json(text, schema_desc)
|
|
1025
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
1026
|
+
except Exception as e:
|
|
1027
|
+
return [TextContent(type="text", text=f"提取失败: {str(e)}")]
|
|
1028
|
+
|
|
1029
|
+
elif name == "get_capabilities":
|
|
1030
|
+
try:
|
|
1031
|
+
import json
|
|
1032
|
+
caps = get_capabilities()
|
|
1033
|
+
return [TextContent(type="text", text=json.dumps(caps, ensure_ascii=False, indent=2))]
|
|
1034
|
+
except Exception as e:
|
|
1035
|
+
return [TextContent(type="text", text=f"获取能力失败: {str(e)}")]
|
|
1036
|
+
|
|
1037
|
+
return [TextContent(type="text", text=f"未知工具: {name}")]
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
async def main_stdio():
|
|
1041
|
+
"""以 stdio 模式启动 MCP Server(Claude Code 自动管理)"""
|
|
1042
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
1043
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def main_sse(host: str = "0.0.0.0", port: int = 8765):
|
|
1047
|
+
"""
|
|
1048
|
+
以 SSE 模式启动 MCP Server(独立 HTTP 服务)
|
|
1049
|
+
|
|
1050
|
+
启动: python -m maque.mcp_server --sse --port 8765
|
|
1051
|
+
配置: claude mcp add maque-remote --transport sse --url http://localhost:8765/sse
|
|
1052
|
+
"""
|
|
1053
|
+
from mcp.server.sse import SseServerTransport
|
|
1054
|
+
from starlette.applications import Starlette
|
|
1055
|
+
from starlette.routing import Route
|
|
1056
|
+
import uvicorn
|
|
1057
|
+
|
|
1058
|
+
sse = SseServerTransport("/messages")
|
|
1059
|
+
|
|
1060
|
+
async def handle_sse(request):
|
|
1061
|
+
async with sse.connect_sse(
|
|
1062
|
+
request.scope, request.receive, request._send
|
|
1063
|
+
) as streams:
|
|
1064
|
+
await server.run(
|
|
1065
|
+
streams[0], streams[1], server.create_initialization_options()
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
async def handle_messages(request):
|
|
1069
|
+
await sse.handle_post_message(request.scope, request.receive, request._send)
|
|
1070
|
+
|
|
1071
|
+
app = Starlette(
|
|
1072
|
+
routes=[
|
|
1073
|
+
Route("/sse", endpoint=handle_sse),
|
|
1074
|
+
Route("/messages", endpoint=handle_messages, methods=["POST"]),
|
|
1075
|
+
]
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
print(f"🚀 MCP Server (SSE) running at http://{host}:{port}")
|
|
1079
|
+
print(f" 配置命令: claude mcp add maque --transport sse --url http://localhost:{port}/sse")
|
|
1080
|
+
uvicorn.run(app, host=host, port=port)
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
if __name__ == "__main__":
|
|
1084
|
+
import sys
|
|
1085
|
+
import asyncio
|
|
1086
|
+
|
|
1087
|
+
if "--sse" in sys.argv:
|
|
1088
|
+
# SSE 模式:独立 HTTP 服务
|
|
1089
|
+
port = 8765
|
|
1090
|
+
for i, arg in enumerate(sys.argv):
|
|
1091
|
+
if arg == "--port" and i + 1 < len(sys.argv):
|
|
1092
|
+
port = int(sys.argv[i + 1])
|
|
1093
|
+
main_sse(port=port)
|
|
1094
|
+
else:
|
|
1095
|
+
# stdio 模式:Claude Code 自动管理
|
|
1096
|
+
asyncio.run(main_stdio())
|