mcai-knowledge 0.2.0__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.
- mcai/__init__.py +3 -0
- mcai/__main__.py +7 -0
- mcai/autopilot.py +229 -0
- mcai/cli.py +558 -0
- mcai/corpus.py +224 -0
- mcai/dashboard.py +229 -0
- mcai/dataset.py +203 -0
- mcai/deploy.py +106 -0
- mcai/disambig.py +71 -0
- mcai/extract.py +757 -0
- mcai/factory.py +223 -0
- mcai/guide.py +83 -0
- mcai/knowledge.py +515 -0
- mcai/llm.py +97 -0
- mcai/manifest.py +171 -0
- mcai/mcp_server.py +280 -0
- mcai/pdftext.py +214 -0
- mcai/pipeline.py +47 -0
- mcai/probe.py +230 -0
- mcai/quality.py +165 -0
- mcai/schema.py +168 -0
- mcai/selftest.py +74 -0
- mcai/server.py +426 -0
- mcai/site.py +497 -0
- mcai/snapshot.py +176 -0
- mcai/telemetry.py +162 -0
- mcai/templates/config/corpus.json +78 -0
- mcai/templates/config/curated.json +500 -0
- mcai/templates/config/disambiguation.json +76 -0
- mcai/templates/config/llm.json +9 -0
- mcai/templates/config/overrides.json +19 -0
- mcai/text.py +61 -0
- mcai/workspace.py +97 -0
- mcai_knowledge-0.2.0.dist-info/METADATA +606 -0
- mcai_knowledge-0.2.0.dist-info/RECORD +39 -0
- mcai_knowledge-0.2.0.dist-info/WHEEL +4 -0
- mcai_knowledge-0.2.0.dist-info/entry_points.txt +3 -0
- mcai_knowledge-0.2.0.dist-info/licenses/LICENSE +21 -0
- mcai_knowledge-0.2.0.dist-info/licenses/LICENSE-CONTENT +24 -0
mcai/manifest.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""对外可发现性清单:让别的 AI / Agent 能自动找到并接入。
|
|
2
|
+
|
|
3
|
+
生成三个机器可读文件:
|
|
4
|
+
- /.well-known/mcp.json MCP 服务器清单(传输方式、工具、示例)
|
|
5
|
+
- /.well-known/agent.json Agent Card(能力、技能、入口)
|
|
6
|
+
- /openapi.json HTTP API 的 OpenAPI 3.1 描述
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .extract import KnowledgeBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _base(project: dict) -> str:
|
|
14
|
+
return project["site_url"].rstrip("/")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def mcp_manifest(kb: KnowledgeBase, project: dict) -> dict:
|
|
18
|
+
from .mcp_server import PROTOCOL_VERSION, SERVER_INFO, TOOLS
|
|
19
|
+
|
|
20
|
+
base = _base(project)
|
|
21
|
+
return {
|
|
22
|
+
"name": SERVER_INFO["name"],
|
|
23
|
+
"displayName": f"{project['brand']}学说知识库",
|
|
24
|
+
"description": (
|
|
25
|
+
f"可被其他 Agent 调用的{project['brand']}学说知识库:"
|
|
26
|
+
f"{kb.stats['concepts']} 个术语定义、{kb.stats['faqs']} 组问答、"
|
|
27
|
+
f"{kb.stats['relations']} 条概念关系,全部带原文出处。"
|
|
28
|
+
),
|
|
29
|
+
"version": SERVER_INFO["version"],
|
|
30
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
31
|
+
"transport": {"type": "stdio", "command": "python", "args": ["run.py", "mcp"]},
|
|
32
|
+
"capabilities": {"tools": True, "resources": True, "prompts": False},
|
|
33
|
+
"tools": [
|
|
34
|
+
{
|
|
35
|
+
"name": t["name"],
|
|
36
|
+
"description": t["description"],
|
|
37
|
+
"inputSchema": t["inputSchema"],
|
|
38
|
+
}
|
|
39
|
+
for t in TOOLS
|
|
40
|
+
],
|
|
41
|
+
"http": {
|
|
42
|
+
"base": f"{base}/api/v1",
|
|
43
|
+
"endpoints": ["health", "catalog", "search", "concept/<slug>", "answer", "answer/batch",
|
|
44
|
+
"related"],
|
|
45
|
+
"localOnly": ["stats", "gaps"],
|
|
46
|
+
"auth": "none (local) / Bearer token (public)",
|
|
47
|
+
},
|
|
48
|
+
"examples": [
|
|
49
|
+
{"tool": "mcai_answer", "arguments": {"question": "什么是元意识?"}},
|
|
50
|
+
{"tool": "mcai_search", "arguments": {"query": "序生", "limit": 5}},
|
|
51
|
+
{"tool": "mcai_related", "arguments": {"term": "元意识", "depth": 1}},
|
|
52
|
+
],
|
|
53
|
+
"license": project.get("license", ""),
|
|
54
|
+
"publisher": project.get("org", ""),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def agent_card(kb: KnowledgeBase, project: dict) -> dict:
|
|
59
|
+
base = _base(project)
|
|
60
|
+
return {
|
|
61
|
+
"name": f"{project['brand']}知识 Agent",
|
|
62
|
+
"description": (
|
|
63
|
+
f"回答关于{project['brand']}学说的问题,提供术语定义、概念关系与原文引证;"
|
|
64
|
+
"不臆造理论,资料未涉及时会明确说明。"
|
|
65
|
+
),
|
|
66
|
+
"url": base,
|
|
67
|
+
"version": project.get("version", "0.1.0"),
|
|
68
|
+
"provider": {"organization": project.get("org", ""), "url": base},
|
|
69
|
+
"capabilities": {"streaming": False, "pushNotifications": False, "stateTransitionHistory": False},
|
|
70
|
+
"defaultInputModes": ["text/plain"],
|
|
71
|
+
"defaultOutputModes": ["application/json", "text/markdown"],
|
|
72
|
+
"skills": [
|
|
73
|
+
{
|
|
74
|
+
"id": "mcai_answer",
|
|
75
|
+
"name": "学说问答",
|
|
76
|
+
"description": "基于术语表与文献引证回答问题,返回定义、出处与相关概念。",
|
|
77
|
+
"examples": ["什么是元意识?", "序生智能体与AI涌现是什么关系?"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "mcai_search",
|
|
81
|
+
"name": "术语检索",
|
|
82
|
+
"description": "按中文、英文或别名检索术语。",
|
|
83
|
+
"examples": ["搜索 元知", "search meta-consciousness"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "mcai_related",
|
|
87
|
+
"name": "概念关系",
|
|
88
|
+
"description": "返回概念的关系子图,用于解释结构。",
|
|
89
|
+
"examples": ["元意识和元态能量是什么关系?"],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
"endpoints": {
|
|
93
|
+
"mcp": f"{base}/.well-known/mcp.json",
|
|
94
|
+
"http": f"{base}/api/v1",
|
|
95
|
+
"openapi": f"{base}/openapi.json",
|
|
96
|
+
"llms": f"{base}/llms.txt",
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def openapi(kb: KnowledgeBase, project: dict) -> dict:
|
|
102
|
+
base = _base(project)
|
|
103
|
+
|
|
104
|
+
def ok(schema: dict) -> dict:
|
|
105
|
+
return {"200": {"description": "OK", "content": {"application/json": {"schema": schema}}} }
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"openapi": "3.1.0",
|
|
109
|
+
"info": {
|
|
110
|
+
"title": f"{project['brand']}知识库 API",
|
|
111
|
+
"version": project.get("version", "0.1.0"),
|
|
112
|
+
"description": f"术语、问答与概念关系查询。共 {kb.stats['concepts']} 个术语。",
|
|
113
|
+
},
|
|
114
|
+
"servers": [{"url": f"{base}/api/v1"}],
|
|
115
|
+
"paths": {
|
|
116
|
+
"/health": {"get": {"summary": "健康检查", "responses": ok({"type": "object"})}},
|
|
117
|
+
"/catalog": {
|
|
118
|
+
"get": {
|
|
119
|
+
"summary": "术语目录",
|
|
120
|
+
"parameters": [{"name": "category", "in": "query", "schema": {"type": "string"}}],
|
|
121
|
+
"responses": ok({"type": "object"}),
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"/search": {
|
|
125
|
+
"get": {
|
|
126
|
+
"summary": "检索术语",
|
|
127
|
+
"parameters": [
|
|
128
|
+
{"name": "q", "in": "query", "required": True, "schema": {"type": "string"}},
|
|
129
|
+
{"name": "limit", "in": "query", "schema": {"type": "integer", "default": 8}},
|
|
130
|
+
],
|
|
131
|
+
"responses": ok({"type": "object"}),
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"/concept/{slug}": {
|
|
135
|
+
"get": {
|
|
136
|
+
"summary": "获取词条",
|
|
137
|
+
"parameters": [{"name": "slug", "in": "path", "required": True, "schema": {"type": "string"}}],
|
|
138
|
+
"responses": ok({"type": "object"}),
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"/answer": {
|
|
142
|
+
"get": {
|
|
143
|
+
"summary": "问答(GET)",
|
|
144
|
+
"parameters": [{"name": "q", "in": "query", "required": True, "schema": {"type": "string"}}],
|
|
145
|
+
"responses": ok({"type": "object"}),
|
|
146
|
+
},
|
|
147
|
+
"post": {
|
|
148
|
+
"summary": "问答(POST)",
|
|
149
|
+
"requestBody": {
|
|
150
|
+
"content": {"application/json": {"schema": {
|
|
151
|
+
"type": "object", "properties": {"question": {"type": "string"}},
|
|
152
|
+
"required": ["question"],
|
|
153
|
+
}}}
|
|
154
|
+
},
|
|
155
|
+
"responses": ok({"type": "object"}),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"/related": {
|
|
159
|
+
"get": {
|
|
160
|
+
"summary": "关系子图",
|
|
161
|
+
"parameters": [
|
|
162
|
+
{"name": "term", "in": "query", "required": True, "schema": {"type": "string"}},
|
|
163
|
+
{"name": "depth", "in": "query", "schema": {"type": "integer", "enum": [1, 2]}},
|
|
164
|
+
],
|
|
165
|
+
"responses": ok({"type": "object"}),
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"/stats": {"get": {"summary": "调用统计", "responses": ok({"type": "object"})}},
|
|
169
|
+
"/gaps": {"get": {"summary": "未命中的问题(用于补词)", "responses": ok({"type": "object"})}},
|
|
170
|
+
},
|
|
171
|
+
}
|
mcai/mcp_server.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""MCP(Model Context Protocol)服务器:stdio + JSON-RPC 2.0。
|
|
2
|
+
|
|
3
|
+
让 Claude Desktop / Cursor / 其他 Agent 把元意识知识库当作工具与资源直接调用。
|
|
4
|
+
启动:python run.py mcp
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import knowledge
|
|
13
|
+
|
|
14
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
15
|
+
SERVER_INFO = {"name": "mcai-knowledge", "version": "0.2.0"}
|
|
16
|
+
|
|
17
|
+
TOOLS = [
|
|
18
|
+
{
|
|
19
|
+
"name": "mcai_answer",
|
|
20
|
+
"description": (
|
|
21
|
+
"回答关于元意识学说的问题。返回权威定义、原文引证与相关概念。"
|
|
22
|
+
"适合回答“什么是元意识”“序生智能体与AI涌现的关系”等问题。"
|
|
23
|
+
),
|
|
24
|
+
"inputSchema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"question": {"type": "string", "description": "用户问题,中英文均可"},
|
|
28
|
+
},
|
|
29
|
+
"required": ["question"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "mcai_concept",
|
|
34
|
+
"description": "按术语名、别名或 slug 获取单个概念的完整词条(定义、别名、出处、引证、关系)。",
|
|
35
|
+
"inputSchema": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"term": {"type": "string", "description": "术语、别名或 slug,如 元意识 / MCAI / meta-consciousness"},
|
|
39
|
+
},
|
|
40
|
+
"required": ["term"],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "mcai_search",
|
|
45
|
+
"description": "在术语库中检索概念,支持中文关键词、英文词与别名,可按体系过滤。",
|
|
46
|
+
"inputSchema": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"query": {"type": "string"},
|
|
50
|
+
"limit": {"type": "integer", "default": 8, "minimum": 1, "maximum": 30},
|
|
51
|
+
"category": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "可选:元意识体系 / 本体论模型 / 序生体系 / 元子社会",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"required": ["query"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "mcai_related",
|
|
61
|
+
"description": "获取概念的关系子图(含衍生关系),用于解释概念之间的因果与结构。",
|
|
62
|
+
"inputSchema": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"term": {"type": "string"},
|
|
66
|
+
"depth": {"type": "integer", "default": 1, "minimum": 1, "maximum": 2},
|
|
67
|
+
},
|
|
68
|
+
"required": ["term"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "mcai_list",
|
|
73
|
+
"description": "列出全部术语(可按体系过滤),用于了解知识库覆盖范围。",
|
|
74
|
+
"inputSchema": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"properties": {
|
|
77
|
+
"category": {"type": "string"},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
PROMPTS = [
|
|
85
|
+
{
|
|
86
|
+
"name": "explain_concept",
|
|
87
|
+
"description": "用元意识学说的定义解释一个术语,要求区分同名概念。",
|
|
88
|
+
"arguments": [{"name": "term", "description": "术语名", "required": True}],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "analyze_phenomenon",
|
|
92
|
+
"description": "用元意识学说的框架分析一个现象,先取定义再推理。",
|
|
93
|
+
"arguments": [{"name": "phenomenon", "description": "现象或问题", "required": True}],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "compare_terms",
|
|
97
|
+
"description": "比较两个术语,先取两者定义与关系再作答。",
|
|
98
|
+
"arguments": [
|
|
99
|
+
{"name": "a", "description": "术语 A", "required": True},
|
|
100
|
+
{"name": "b", "description": "术语 B", "required": True},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def _text(payload: dict | list | str) -> dict:
|
|
106
|
+
body = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False, indent=2)
|
|
107
|
+
return {"content": [{"type": "text", "text": body}], "isError": False}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _error(message: str) -> dict:
|
|
111
|
+
return {"content": [{"type": "text", "text": message}], "isError": True}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def call_tool(kb: knowledge.Knowledge, name: str, args: dict) -> dict:
|
|
115
|
+
if name == "mcai_answer":
|
|
116
|
+
return _text(kb.answer(str(args.get("question", ""))))
|
|
117
|
+
if name == "mcai_concept":
|
|
118
|
+
concept = kb.resolve(str(args.get("term", "")))
|
|
119
|
+
if not concept:
|
|
120
|
+
return _error(f"未找到术语:{args.get('term', '')}")
|
|
121
|
+
return _text({k: concept.get(k) for k in (
|
|
122
|
+
"id", "term", "en", "aliases", "category", "kind", "parent",
|
|
123
|
+
"definition", "short_definition", "status", "sources", "evidence", "relations",
|
|
124
|
+
)})
|
|
125
|
+
if name == "mcai_search":
|
|
126
|
+
limit = int(args.get("limit", 8) or 8)
|
|
127
|
+
return _text({"query": args.get("query", ""),
|
|
128
|
+
"results": kb.search(str(args.get("query", "")), limit=limit,
|
|
129
|
+
category=args.get("category"))})
|
|
130
|
+
if name == "mcai_related":
|
|
131
|
+
depth = int(args.get("depth", 1) or 1)
|
|
132
|
+
return _text(kb.related(str(args.get("term", "")), depth=depth))
|
|
133
|
+
if name == "mcai_list":
|
|
134
|
+
return _text(kb.catalog(args.get("category")))
|
|
135
|
+
return _error(f"未知工具:{name}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def handle(kb: knowledge.Knowledge, request: dict, tele=None) -> dict | None:
|
|
139
|
+
"""处理单条 JSON-RPC 请求;通知类返回 None。"""
|
|
140
|
+
method = request.get("method", "")
|
|
141
|
+
req_id = request.get("id")
|
|
142
|
+
params = request.get("params") or {}
|
|
143
|
+
|
|
144
|
+
if method == "initialize":
|
|
145
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {
|
|
146
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
147
|
+
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
|
|
148
|
+
"serverInfo": SERVER_INFO,
|
|
149
|
+
}}
|
|
150
|
+
if method in ("notifications/initialized", "notifications/cancelled"):
|
|
151
|
+
return None
|
|
152
|
+
if method == "ping":
|
|
153
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {}}
|
|
154
|
+
if method == "tools/list":
|
|
155
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}}
|
|
156
|
+
if method == "tools/call":
|
|
157
|
+
name = params.get("name", "")
|
|
158
|
+
args = params.get("arguments") or {}
|
|
159
|
+
try:
|
|
160
|
+
result = call_tool(kb, name, args)
|
|
161
|
+
if tele is not None:
|
|
162
|
+
query = str(args.get("question") or args.get("term") or args.get("query") or "")
|
|
163
|
+
rtype = ""
|
|
164
|
+
if name == "mcai_answer" and not result.get("isError"):
|
|
165
|
+
try:
|
|
166
|
+
rtype = json.loads(result["content"][0]["text"]).get("type", "")
|
|
167
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
168
|
+
rtype = ""
|
|
169
|
+
tele.record("mcp", name, query, rtype, ok=not result.get("isError", False))
|
|
170
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
171
|
+
except Exception as exc: # noqa: BLE001
|
|
172
|
+
return {"jsonrpc": "2.0", "id": req_id,
|
|
173
|
+
"result": _error(f"工具执行失败:{type(exc).__name__}: {exc}")}
|
|
174
|
+
if method == "prompts/list":
|
|
175
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"prompts": PROMPTS}}
|
|
176
|
+
if method == "prompts/get":
|
|
177
|
+
name = params.get("name", "")
|
|
178
|
+
args = params.get("arguments") or {}
|
|
179
|
+
if name == "explain_concept":
|
|
180
|
+
term = args.get("term", "元意识")
|
|
181
|
+
text = (f"请调用 mcai_concept 获取「{term}」的定义与引证,然后用该学说的术语解释它。"
|
|
182
|
+
f"必须先说明它不是什么(若有消歧信息),再给出定义。")
|
|
183
|
+
elif name == "analyze_phenomenon":
|
|
184
|
+
phenomenon = args.get("phenomenon", "")
|
|
185
|
+
text = (f"请先用 mcai_search 检索与「{phenomenon}」相关的术语,"
|
|
186
|
+
f"再用 mcai_answer 获取定义与出处,最后用该学说框架分析这个现象。"
|
|
187
|
+
f"引用必须带出处,不确定时说明知识库未涉及。")
|
|
188
|
+
elif name == "compare_terms":
|
|
189
|
+
text = (f"请分别调用 mcai_concept 获取「{args.get('a', '')}」与「{args.get('b', '')}」的定义,"
|
|
190
|
+
f"再调用 mcai_related 查看两者关系,最后按「相同点 / 不同点 / 关系类型」作答。")
|
|
191
|
+
else:
|
|
192
|
+
return {"jsonrpc": "2.0", "id": req_id,
|
|
193
|
+
"error": {"code": -32602, "message": f"未知 prompt:{name}"}}
|
|
194
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {
|
|
195
|
+
"description": next((p["description"] for p in PROMPTS if p["name"] == name), ""),
|
|
196
|
+
"messages": [{"role": "user", "content": {"type": "text", "text": text}}],
|
|
197
|
+
}}
|
|
198
|
+
if method == "resources/list":
|
|
199
|
+
resources = [{
|
|
200
|
+
"uri": "mcai://guide",
|
|
201
|
+
"name": "Agent 接入指南",
|
|
202
|
+
"description": "何时调用、返回结构、术语边界与引用规范",
|
|
203
|
+
"mimeType": "text/markdown",
|
|
204
|
+
}] + [
|
|
205
|
+
{
|
|
206
|
+
"uri": f"mcai://concepts/{c['slug']}",
|
|
207
|
+
"name": c["term"],
|
|
208
|
+
"description": c.get("short_definition", ""),
|
|
209
|
+
"mimeType": "text/markdown",
|
|
210
|
+
}
|
|
211
|
+
for c in kb.concepts
|
|
212
|
+
]
|
|
213
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"resources": resources}}
|
|
214
|
+
if method == "resources/read":
|
|
215
|
+
uri = params.get("uri", "")
|
|
216
|
+
if uri == "mcai://guide":
|
|
217
|
+
from .guide import build_guide
|
|
218
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"contents": [{
|
|
219
|
+
"uri": uri, "mimeType": "text/markdown",
|
|
220
|
+
"text": build_guide(kb, kb.payload.get("meta", {}).get("project", {})),
|
|
221
|
+
}]}}
|
|
222
|
+
slug = uri.rsplit("/", 1)[-1] if uri.startswith("mcai://concepts/") else uri
|
|
223
|
+
concept = kb.by_slug.get(slug)
|
|
224
|
+
if not concept:
|
|
225
|
+
return {"jsonrpc": "2.0", "id": req_id,
|
|
226
|
+
"error": {"code": -32602, "message": f"未知资源:{uri}"}}
|
|
227
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"contents": [{
|
|
228
|
+
"uri": uri,
|
|
229
|
+
"mimeType": "text/markdown",
|
|
230
|
+
"text": _concept_markdown(concept),
|
|
231
|
+
}]}}
|
|
232
|
+
return {"jsonrpc": "2.0", "id": req_id,
|
|
233
|
+
"error": {"code": -32601, "message": f"不支持的方法:{method}"}}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _concept_markdown(c: dict) -> str:
|
|
237
|
+
lines = [f"# {c['term']}", ""]
|
|
238
|
+
if c.get("en"):
|
|
239
|
+
lines.append(f"*{c['en']}*")
|
|
240
|
+
if c.get("aliases"):
|
|
241
|
+
lines.append(f"别名:{'、'.join(c['aliases'])}")
|
|
242
|
+
lines += ["", c["definition"], "", f"分类:{c['category']}"]
|
|
243
|
+
if c.get("parent"):
|
|
244
|
+
lines.append(f"衍生自:{c['parent']}")
|
|
245
|
+
if c.get("sources"):
|
|
246
|
+
lines += ["", "## 定义来源"]
|
|
247
|
+
lines += [f"- {s['doc_title']}({s.get('section', '')})" for s in c["sources"]]
|
|
248
|
+
if c.get("evidence"):
|
|
249
|
+
lines += ["", "## 文献引证"]
|
|
250
|
+
lines += [f"- 「{e['quote']}」——{e['doc_title']}" for e in c["evidence"][:3]]
|
|
251
|
+
if c.get("relations"):
|
|
252
|
+
lines += ["", "## 相关概念"]
|
|
253
|
+
lines += [f"- {r['target']}({r['type']})" for r in c["relations"][:10]]
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main(root: str | Path) -> None:
|
|
258
|
+
from .telemetry import for_root
|
|
259
|
+
kb = knowledge.load(root)
|
|
260
|
+
tele = for_root(root)
|
|
261
|
+
for line in sys.stdin:
|
|
262
|
+
line = line.strip()
|
|
263
|
+
if not line:
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
request = json.loads(line)
|
|
267
|
+
except json.JSONDecodeError:
|
|
268
|
+
print(json.dumps({"jsonrpc": "2.0", "id": None,
|
|
269
|
+
"error": {"code": -32700, "message": "JSON 解析失败"}}, ensure_ascii=False),
|
|
270
|
+
flush=True)
|
|
271
|
+
continue
|
|
272
|
+
response = handle(kb, request, tele)
|
|
273
|
+
if response is not None:
|
|
274
|
+
print(json.dumps(response, ensure_ascii=False), flush=True)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main_entry() -> None:
|
|
278
|
+
"""控制台入口(mcai-mcp):按 MCAI_HOME / 当前目录解析工作区。"""
|
|
279
|
+
from .workspace import resolve_home
|
|
280
|
+
main(resolve_home(None))
|
mcai/pdftext.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""轻量 PDF 文本抽取:支持 Type0 + ToUnicode 的常见导出(WPS/Word)。
|
|
2
|
+
|
|
3
|
+
只依赖标准库;按文本基线重建行,保持流式阅读顺序。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import zlib
|
|
9
|
+
|
|
10
|
+
_OBJ = re.compile(rb"(\d+)\s+(\d+)\s+obj\b")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _objects(data: bytes) -> dict[int, bytes]:
|
|
14
|
+
spans = list(_OBJ.finditer(data))
|
|
15
|
+
out: dict[int, bytes] = {}
|
|
16
|
+
for i, m in enumerate(spans):
|
|
17
|
+
start = m.end()
|
|
18
|
+
end = data.find(b"endobj", start)
|
|
19
|
+
if end == -1:
|
|
20
|
+
end = spans[i + 1].start() if i + 1 < len(spans) else len(data)
|
|
21
|
+
out[int(m.group(1))] = data[start:end]
|
|
22
|
+
return out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _stream(body: bytes) -> bytes | None:
|
|
26
|
+
m = re.search(rb"stream\r?\n", body)
|
|
27
|
+
if not m:
|
|
28
|
+
return None
|
|
29
|
+
head, raw = body[: m.start()], body[m.end():]
|
|
30
|
+
end = raw.rfind(b"endstream")
|
|
31
|
+
if end != -1:
|
|
32
|
+
raw = raw[:end]
|
|
33
|
+
raw = raw.rstrip(b"\r\n")
|
|
34
|
+
if b"FlateDecode" in head:
|
|
35
|
+
for candidate in (raw, raw.rstrip(b"\n").rstrip(b"\r")):
|
|
36
|
+
try:
|
|
37
|
+
return zlib.decompress(candidate)
|
|
38
|
+
except zlib.error:
|
|
39
|
+
continue
|
|
40
|
+
return None
|
|
41
|
+
return raw
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _utf16(hexstr: bytes | str) -> str:
|
|
45
|
+
h = hexstr.decode("ascii") if isinstance(hexstr, bytes) else hexstr
|
|
46
|
+
if len(h) % 4:
|
|
47
|
+
h = h.zfill((len(h) // 4 + 1) * 4)
|
|
48
|
+
try:
|
|
49
|
+
return bytes.fromhex(h).decode("utf-16-be")
|
|
50
|
+
except (ValueError, UnicodeDecodeError):
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cmap(text: bytes) -> dict[int, str]:
|
|
55
|
+
table: dict[int, str] = {}
|
|
56
|
+
for block in re.findall(rb"beginbfchar(.*?)endbfchar", text, re.S):
|
|
57
|
+
for src, dst in re.findall(rb"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>", block):
|
|
58
|
+
table[int(src, 16)] = _utf16(dst)
|
|
59
|
+
for block in re.findall(rb"beginbfrange(.*?)endbfrange", text, re.S):
|
|
60
|
+
pattern = rb"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*(<[0-9A-Fa-f]+>|\[[^\]]*\])"
|
|
61
|
+
for m in re.finditer(pattern, block, re.S):
|
|
62
|
+
lo, hi, dst = int(m.group(1), 16), int(m.group(2), 16), m.group(3)
|
|
63
|
+
if dst.startswith(b"["):
|
|
64
|
+
for k, item in enumerate(re.findall(rb"<([0-9A-Fa-f]+)>", dst)):
|
|
65
|
+
if lo + k > hi:
|
|
66
|
+
break
|
|
67
|
+
table[lo + k] = _utf16(item)
|
|
68
|
+
else:
|
|
69
|
+
base = _utf16(dst[1:-1])
|
|
70
|
+
for code in range(lo, hi + 1):
|
|
71
|
+
if not base:
|
|
72
|
+
table[code] = ""
|
|
73
|
+
elif code == lo:
|
|
74
|
+
table[code] = base
|
|
75
|
+
else:
|
|
76
|
+
table[code] = base[:-1] + chr(ord(base[-1]) + code - lo)
|
|
77
|
+
return table
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
_TOKEN = re.compile(
|
|
81
|
+
rb"""
|
|
82
|
+
/(?P<font>[A-Za-z0-9_.+-]+)\s+(?P<fsize>[\d.]+)\s+Tf
|
|
83
|
+
| (?P<e>-?[\d.]+)\s+(?P<f>-?[\d.]+)\s+Tm
|
|
84
|
+
| (?P<ty>-?[\d.]+)\s+(?P<tdop>Td|TD)
|
|
85
|
+
| (?P<tj>\[(?P<arr>[^\]]*)\]\s*TJ)
|
|
86
|
+
| (?P<tj1>\((?:\\.|[^\\)])*\)\s*Tj)
|
|
87
|
+
| (?P<tj2><[0-9A-Fa-f\s]*>\s*Tj)
|
|
88
|
+
""",
|
|
89
|
+
re.S | re.X,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _decode_hex(value: bytes, table: dict[int, str]) -> str:
|
|
94
|
+
h = re.sub(rb"\s", b"", value).decode("ascii")
|
|
95
|
+
if len(h) % 2:
|
|
96
|
+
h += "0"
|
|
97
|
+
raw = bytes.fromhex(h)
|
|
98
|
+
return "".join(table.get((raw[i] << 8) | raw[i + 1], "") for i in range(0, len(raw) - 1, 2))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _decode_lit(value: bytes, table: dict[int, str]) -> str:
|
|
102
|
+
s = value.replace(rb"\(", b"(").replace(rb"\)", b")").replace(rb"\\", b"\\")
|
|
103
|
+
if len(s) % 2:
|
|
104
|
+
return s.decode("latin1", "replace")
|
|
105
|
+
return "".join(table.get((s[i] << 8) | s[i + 1], "") for i in range(0, len(s) - 1, 2))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _fonts(res: bytes) -> dict[str, int]:
|
|
109
|
+
out: dict[str, int] = {}
|
|
110
|
+
m = re.search(rb"/Font\s*<<(.*?)>>", res, re.S)
|
|
111
|
+
if m:
|
|
112
|
+
for name, num in re.findall(rb"/([A-Za-z0-9_.+-]+)\s+(\d+)\s+0\s+R", m.group(1)):
|
|
113
|
+
out[name.decode()] = int(num)
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _pages(objects: dict[int, bytes]) -> list[int]:
|
|
118
|
+
catalog = next((n for n, b in objects.items() if re.search(rb"/Type\s*/Catalog", b)), None)
|
|
119
|
+
found: list[int] = []
|
|
120
|
+
|
|
121
|
+
def walk(num: int) -> None:
|
|
122
|
+
body = objects.get(num, b"")
|
|
123
|
+
if re.search(rb"/Type\s*/Pages", body):
|
|
124
|
+
kids = re.search(rb"/Kids\s*\[(.*?)\]", body, re.S)
|
|
125
|
+
if kids:
|
|
126
|
+
for k in re.findall(rb"(\d+)\s+0\s+R", kids.group(1)):
|
|
127
|
+
walk(int(k))
|
|
128
|
+
elif re.search(rb"/Type\s*/Page\b", body):
|
|
129
|
+
found.append(num)
|
|
130
|
+
|
|
131
|
+
if catalog is not None:
|
|
132
|
+
root = re.search(rb"/Pages\s+(\d+)\s+0\s+R", objects[catalog])
|
|
133
|
+
if root:
|
|
134
|
+
walk(int(root.group(1)))
|
|
135
|
+
return found
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_pdf_text(path: str) -> list[str]:
|
|
139
|
+
"""返回每一页的文本(按行)。"""
|
|
140
|
+
data = open(path, "rb").read()
|
|
141
|
+
objects = _objects(data)
|
|
142
|
+
|
|
143
|
+
cmaps: dict[int, dict[int, str]] = {}
|
|
144
|
+
for num, body in objects.items():
|
|
145
|
+
ref = re.search(rb"/ToUnicode\s+(\d+)\s+0\s+R", body)
|
|
146
|
+
if ref:
|
|
147
|
+
raw = _stream(objects.get(int(ref.group(1)), b""))
|
|
148
|
+
if raw:
|
|
149
|
+
cmaps[num] = _cmap(raw)
|
|
150
|
+
|
|
151
|
+
pages_out: list[str] = []
|
|
152
|
+
for page in _pages(objects):
|
|
153
|
+
body = objects[page]
|
|
154
|
+
res = b""
|
|
155
|
+
ref = re.search(rb"/Resources\s+(\d+)\s+0\s+R", body)
|
|
156
|
+
if ref:
|
|
157
|
+
res = objects.get(int(ref.group(1)), b"")
|
|
158
|
+
else:
|
|
159
|
+
inline = re.search(rb"/Resources\s*<<(.*)", body, re.S)
|
|
160
|
+
if inline:
|
|
161
|
+
res = inline.group(1)
|
|
162
|
+
font_map = _fonts(res)
|
|
163
|
+
|
|
164
|
+
refs: list[int] = []
|
|
165
|
+
one = re.search(rb"/Contents\s+(\d+)\s+0\s+R", body)
|
|
166
|
+
if one:
|
|
167
|
+
refs = [int(one.group(1))]
|
|
168
|
+
else:
|
|
169
|
+
many = re.search(rb"/Contents\s*\[(.*?)\]", body, re.S)
|
|
170
|
+
if many:
|
|
171
|
+
refs = [int(x) for x in re.findall(rb"(\d+)\s+0\s+R", many.group(1))]
|
|
172
|
+
stream = b"".join((_stream(objects.get(r, b"")) or b"") + b"\n" for r in refs)
|
|
173
|
+
|
|
174
|
+
y = 0.0
|
|
175
|
+
last_y: float | None = None
|
|
176
|
+
table: dict[int, str] = {}
|
|
177
|
+
buf: list[str] = []
|
|
178
|
+
for m in _TOKEN.finditer(stream):
|
|
179
|
+
if m.group("font"):
|
|
180
|
+
table = cmaps.get(font_map.get(m.group("font").decode(), -1), {})
|
|
181
|
+
elif m.group("f") is not None:
|
|
182
|
+
y = float(m.group("f"))
|
|
183
|
+
elif m.group("tdop"):
|
|
184
|
+
y += float(m.group("ty"))
|
|
185
|
+
elif m.group("tj") or m.group("tj1") or m.group("tj2"):
|
|
186
|
+
if m.group("tj"):
|
|
187
|
+
text = ""
|
|
188
|
+
parts = re.findall(rb"<([0-9A-Fa-f\s]*)>|\(((?:\\.|[^\\)])*)\)|(-?[\d.]+)", m.group("arr"), re.S)
|
|
189
|
+
for hx, lit, num in parts:
|
|
190
|
+
if hx:
|
|
191
|
+
text += _decode_hex(hx, table)
|
|
192
|
+
elif lit:
|
|
193
|
+
text += _decode_lit(lit, table)
|
|
194
|
+
elif num and float(num) < -120:
|
|
195
|
+
text += " "
|
|
196
|
+
else:
|
|
197
|
+
raw = (m.group("tj1") or m.group("tj2"))
|
|
198
|
+
raw = raw[: raw.rfind(b"Tj")]
|
|
199
|
+
hm = re.search(rb"<([0-9A-Fa-f\s]*)>", raw)
|
|
200
|
+
if hm:
|
|
201
|
+
text = _decode_hex(hm.group(1), table)
|
|
202
|
+
else:
|
|
203
|
+
lm = re.search(rb"\(((?:\\.|[^\\)])*)\)", raw, re.S)
|
|
204
|
+
text = _decode_lit(lm.group(1), table) if lm else ""
|
|
205
|
+
if not text:
|
|
206
|
+
continue
|
|
207
|
+
if last_y is not None and abs(y - last_y) > 3.0:
|
|
208
|
+
buf.append("\n")
|
|
209
|
+
last_y = y
|
|
210
|
+
buf.append(text)
|
|
211
|
+
raw_text = "".join(buf)
|
|
212
|
+
lines = [re.sub(r"[ \t]+", " ", ln).strip() for ln in raw_text.split("\n")]
|
|
213
|
+
pages_out.append("\n".join(ln for ln in lines if ln))
|
|
214
|
+
return pages_out
|