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.
Files changed (143) hide show
  1. maque/__init__.py +30 -0
  2. maque/__main__.py +926 -0
  3. maque/ai_platform/__init__.py +0 -0
  4. maque/ai_platform/crawl.py +45 -0
  5. maque/ai_platform/metrics.py +258 -0
  6. maque/ai_platform/nlp_preprocess.py +67 -0
  7. maque/ai_platform/webpage_screen_shot.py +195 -0
  8. maque/algorithms/__init__.py +78 -0
  9. maque/algorithms/bezier.py +15 -0
  10. maque/algorithms/bktree.py +117 -0
  11. maque/algorithms/core.py +104 -0
  12. maque/algorithms/hilbert.py +16 -0
  13. maque/algorithms/rate_function.py +92 -0
  14. maque/algorithms/transform.py +27 -0
  15. maque/algorithms/trie.py +272 -0
  16. maque/algorithms/utils.py +63 -0
  17. maque/algorithms/video.py +587 -0
  18. maque/api/__init__.py +1 -0
  19. maque/api/common.py +110 -0
  20. maque/api/fetch.py +26 -0
  21. maque/api/static/icon.png +0 -0
  22. maque/api/static/redoc.standalone.js +1782 -0
  23. maque/api/static/swagger-ui-bundle.js +3 -0
  24. maque/api/static/swagger-ui.css +3 -0
  25. maque/cli/__init__.py +1 -0
  26. maque/cli/clean_invisible_chars.py +324 -0
  27. maque/cli/core.py +34 -0
  28. maque/cli/groups/__init__.py +26 -0
  29. maque/cli/groups/config.py +205 -0
  30. maque/cli/groups/data.py +615 -0
  31. maque/cli/groups/doctor.py +259 -0
  32. maque/cli/groups/embedding.py +222 -0
  33. maque/cli/groups/git.py +29 -0
  34. maque/cli/groups/help.py +410 -0
  35. maque/cli/groups/llm.py +223 -0
  36. maque/cli/groups/mcp.py +241 -0
  37. maque/cli/groups/mllm.py +1795 -0
  38. maque/cli/groups/mllm_simple.py +60 -0
  39. maque/cli/groups/quant.py +210 -0
  40. maque/cli/groups/service.py +490 -0
  41. maque/cli/groups/system.py +570 -0
  42. maque/cli/mllm_run.py +1451 -0
  43. maque/cli/script.py +52 -0
  44. maque/cli/tree.py +49 -0
  45. maque/clustering/__init__.py +52 -0
  46. maque/clustering/analyzer.py +347 -0
  47. maque/clustering/clusterers.py +464 -0
  48. maque/clustering/sampler.py +134 -0
  49. maque/clustering/visualizer.py +205 -0
  50. maque/constant.py +13 -0
  51. maque/core.py +133 -0
  52. maque/cv/__init__.py +1 -0
  53. maque/cv/image.py +219 -0
  54. maque/cv/utils.py +68 -0
  55. maque/cv/video/__init__.py +3 -0
  56. maque/cv/video/keyframe_extractor.py +368 -0
  57. maque/embedding/__init__.py +43 -0
  58. maque/embedding/base.py +56 -0
  59. maque/embedding/multimodal.py +308 -0
  60. maque/embedding/server.py +523 -0
  61. maque/embedding/text.py +311 -0
  62. maque/git/__init__.py +24 -0
  63. maque/git/pure_git.py +912 -0
  64. maque/io/__init__.py +29 -0
  65. maque/io/core.py +38 -0
  66. maque/io/ops.py +194 -0
  67. maque/llm/__init__.py +111 -0
  68. maque/llm/backend.py +416 -0
  69. maque/llm/base.py +411 -0
  70. maque/llm/server.py +366 -0
  71. maque/mcp_server.py +1096 -0
  72. maque/mllm_data_processor_pipeline/__init__.py +17 -0
  73. maque/mllm_data_processor_pipeline/core.py +341 -0
  74. maque/mllm_data_processor_pipeline/example.py +291 -0
  75. maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
  76. maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
  77. maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
  78. maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
  79. maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
  80. maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
  81. maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
  82. maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
  83. maque/mllm_data_processor_pipeline/web_app.py +317 -0
  84. maque/nlp/__init__.py +14 -0
  85. maque/nlp/ngram.py +9 -0
  86. maque/nlp/parser.py +63 -0
  87. maque/nlp/risk_matcher.py +543 -0
  88. maque/nlp/sentence_splitter.py +202 -0
  89. maque/nlp/simple_tradition_cvt.py +31 -0
  90. maque/performance/__init__.py +21 -0
  91. maque/performance/_measure_time.py +70 -0
  92. maque/performance/_profiler.py +367 -0
  93. maque/performance/_stat_memory.py +51 -0
  94. maque/pipelines/__init__.py +15 -0
  95. maque/pipelines/clustering.py +252 -0
  96. maque/quantization/__init__.py +42 -0
  97. maque/quantization/auto_round.py +120 -0
  98. maque/quantization/base.py +145 -0
  99. maque/quantization/bitsandbytes.py +127 -0
  100. maque/quantization/llm_compressor.py +102 -0
  101. maque/retriever/__init__.py +35 -0
  102. maque/retriever/chroma.py +654 -0
  103. maque/retriever/document.py +140 -0
  104. maque/retriever/milvus.py +1140 -0
  105. maque/table_ops/__init__.py +1 -0
  106. maque/table_ops/core.py +133 -0
  107. maque/table_viewer/__init__.py +4 -0
  108. maque/table_viewer/download_assets.py +57 -0
  109. maque/table_viewer/server.py +698 -0
  110. maque/table_viewer/static/element-plus-icons.js +5791 -0
  111. maque/table_viewer/static/element-plus.css +1 -0
  112. maque/table_viewer/static/element-plus.js +65236 -0
  113. maque/table_viewer/static/main.css +268 -0
  114. maque/table_viewer/static/main.js +669 -0
  115. maque/table_viewer/static/vue.global.js +18227 -0
  116. maque/table_viewer/templates/index.html +401 -0
  117. maque/utils/__init__.py +56 -0
  118. maque/utils/color.py +68 -0
  119. maque/utils/color_string.py +45 -0
  120. maque/utils/compress.py +66 -0
  121. maque/utils/constant.py +183 -0
  122. maque/utils/core.py +261 -0
  123. maque/utils/cursor.py +143 -0
  124. maque/utils/distance.py +58 -0
  125. maque/utils/docker.py +96 -0
  126. maque/utils/downloads.py +51 -0
  127. maque/utils/excel_helper.py +542 -0
  128. maque/utils/helper_metrics.py +121 -0
  129. maque/utils/helper_parser.py +168 -0
  130. maque/utils/net.py +64 -0
  131. maque/utils/nvidia_stat.py +140 -0
  132. maque/utils/ops.py +53 -0
  133. maque/utils/packages.py +31 -0
  134. maque/utils/path.py +57 -0
  135. maque/utils/tar.py +260 -0
  136. maque/utils/untar.py +129 -0
  137. maque/web/__init__.py +0 -0
  138. maque/web/image_downloader.py +1410 -0
  139. maque-0.2.1.dist-info/METADATA +450 -0
  140. maque-0.2.1.dist-info/RECORD +143 -0
  141. maque-0.2.1.dist-info/WHEEL +4 -0
  142. maque-0.2.1.dist-info/entry_points.txt +3 -0
  143. 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())