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/deploy.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""上线工具:设置站点地址、远程自检。
|
|
2
|
+
|
|
3
|
+
对外暴露前必须把 project.site_url 改成真实地址,否则 .well-known 清单里写的还是本地地址,
|
|
4
|
+
别的 Agent 拿到清单也接不上。doctor 就是用来发现这类不一致的。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import secrets
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_site_url(home: str | Path, url: str) -> dict:
|
|
17
|
+
url = (url or "").strip().rstrip("/")
|
|
18
|
+
if not url.startswith(("http://", "https://")):
|
|
19
|
+
raise ValueError("地址必须以 http:// 或 https:// 开头")
|
|
20
|
+
path = Path(home) / "config" / "corpus.json"
|
|
21
|
+
cfg = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
22
|
+
old = (cfg.get("project") or {}).get("site_url", "")
|
|
23
|
+
cfg.setdefault("project", {})["site_url"] = url
|
|
24
|
+
path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
25
|
+
return {"old": old, "new": url, "path": str(path)}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def new_token() -> str:
|
|
29
|
+
return secrets.token_urlsafe(24)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get(url: str, token: str = "", timeout: int = 10) -> tuple[int, bytes]:
|
|
33
|
+
req = urllib.request.Request(url, headers={"User-Agent": "mcai-doctor/0.1"})
|
|
34
|
+
if token:
|
|
35
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
36
|
+
try:
|
|
37
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
38
|
+
return resp.status, resp.read()
|
|
39
|
+
except urllib.error.HTTPError as exc:
|
|
40
|
+
return exc.code, exc.read()
|
|
41
|
+
except Exception as exc: # noqa: BLE001 - 网络错误统一成状态 0
|
|
42
|
+
return 0, str(exc).encode("utf-8")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def doctor(url: str, token: str = "", timeout: int = 10) -> list[dict]:
|
|
46
|
+
"""对已部署的实例做一轮体检,返回检查项列表。"""
|
|
47
|
+
base = (url or "").strip().rstrip("/")
|
|
48
|
+
checks: list[dict] = []
|
|
49
|
+
|
|
50
|
+
def add(name: str, ok: bool, detail: str, hint: str = "") -> None:
|
|
51
|
+
checks.append({"name": name, "ok": ok, "detail": detail, "hint": hint})
|
|
52
|
+
|
|
53
|
+
# 1. 存活
|
|
54
|
+
status, body = _get(f"{base}/api/v1/health", timeout=timeout)
|
|
55
|
+
health = {}
|
|
56
|
+
if status == 200:
|
|
57
|
+
try:
|
|
58
|
+
health = json.loads(body.decode("utf-8"))
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
health = {}
|
|
61
|
+
add("health", True,
|
|
62
|
+
f"{health.get('concepts')} 概念 / {health.get('faqs')} 问答 · "
|
|
63
|
+
f"auth_required={health.get('auth_required')}")
|
|
64
|
+
else:
|
|
65
|
+
add("health", False, f"HTTP {status}", "服务未启动,或地址/端口不对")
|
|
66
|
+
|
|
67
|
+
# 2. MCP 清单里的地址是否与部署地址一致
|
|
68
|
+
status, body = _get(f"{base}/.well-known/mcp.json", timeout=timeout)
|
|
69
|
+
if status == 200:
|
|
70
|
+
try:
|
|
71
|
+
manifest = json.loads(body.decode("utf-8"))
|
|
72
|
+
manifest_base = (manifest.get("http") or {}).get("base", "")
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
manifest_base = ""
|
|
75
|
+
ok = manifest_base.startswith(base)
|
|
76
|
+
add("manifest", ok, f"清单地址:{manifest_base or '缺失'}",
|
|
77
|
+
"" if ok else "运行 mcai set-url <当前地址> 后重新 build")
|
|
78
|
+
else:
|
|
79
|
+
add("manifest", False, f"HTTP {status}", "尚未构建,或 .well-known 未生成")
|
|
80
|
+
|
|
81
|
+
# 3. 接入指南(给 Agent 读的说明书)
|
|
82
|
+
status, body = _get(f"{base}/llms.txt", timeout=timeout)
|
|
83
|
+
add("llms.txt", status == 200, f"HTTP {status}")
|
|
84
|
+
|
|
85
|
+
# 4. 问答是否可用
|
|
86
|
+
q = urllib.parse.quote("什么是元意识")
|
|
87
|
+
status, body = _get(f"{base}/api/v1/answer?q={q}", token=token, timeout=timeout)
|
|
88
|
+
answer_type = ""
|
|
89
|
+
if status == 200:
|
|
90
|
+
try:
|
|
91
|
+
answer_type = json.loads(body.decode("utf-8")).get("type", "")
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
answer_type = ""
|
|
94
|
+
ok = status == 200 and answer_type in ("concept", "faq", "relation")
|
|
95
|
+
add("answer", ok, f"HTTP {status} · type={answer_type or '-'}",
|
|
96
|
+
"" if ok else ("需要令牌时请加 --token" if status in (401, 403) else "检查知识库是否构建"))
|
|
97
|
+
|
|
98
|
+
# 5. 鉴权是否真的生效
|
|
99
|
+
if token:
|
|
100
|
+
status_anon, _ = _get(f"{base}/api/v1/answer?q={q}", timeout=timeout)
|
|
101
|
+
add("auth", status_anon in (401, 403), f"无令牌访问:HTTP {status_anon}",
|
|
102
|
+
"" if status_anon in (401, 403) else "令牌未生效:检查启动参数 --token")
|
|
103
|
+
else:
|
|
104
|
+
add("auth", True, "未配置令牌(仅建议本机)", "对外暴露请设置 --token")
|
|
105
|
+
|
|
106
|
+
return checks
|
mcai/disambig.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""阶段性消歧层。
|
|
2
|
+
|
|
3
|
+
设计原则:
|
|
4
|
+
- 否定句(“它不是什么”)永远不进入 definition,也不进入概念首屏;
|
|
5
|
+
- 只在特定阶段、以独立区块 / 独立问答 / schema 的 disambiguatingDescription 出现;
|
|
6
|
+
- 整个层可通过 enabled=false 一次性关闭,词条回到中性表述。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Disambiguation:
|
|
15
|
+
def __init__(self, data: dict | None = None):
|
|
16
|
+
data = data or {}
|
|
17
|
+
self.campaign = data.get("campaign", {})
|
|
18
|
+
self.items = {item["term"]: item for item in data.get("items", []) if item.get("term")}
|
|
19
|
+
self.enabled = bool(self.campaign.get("enabled"))
|
|
20
|
+
self.scope = set(self.campaign.get("scope", []))
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def active(self) -> bool:
|
|
24
|
+
return self.enabled and bool(self.items)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def status(self) -> dict:
|
|
28
|
+
return {
|
|
29
|
+
"enabled": self.enabled,
|
|
30
|
+
"campaign": self.campaign.get("name", ""),
|
|
31
|
+
"start": self.campaign.get("start", ""),
|
|
32
|
+
"review_after": self.campaign.get("review_after", ""),
|
|
33
|
+
"items": len(self.items),
|
|
34
|
+
"note": self.campaign.get("note", ""),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def note_for(self, term: str) -> str:
|
|
38
|
+
if not self.active or "schema" not in self.scope:
|
|
39
|
+
return ""
|
|
40
|
+
item = self.items.get(term)
|
|
41
|
+
return item.get("one_line", "") if item else ""
|
|
42
|
+
|
|
43
|
+
def page_block(self, term: str) -> dict | None:
|
|
44
|
+
if not self.active or "concept_page" not in self.scope:
|
|
45
|
+
return None
|
|
46
|
+
return self.items.get(term)
|
|
47
|
+
|
|
48
|
+
def faq_entries(self) -> list[dict]:
|
|
49
|
+
if not self.active or "faq" not in self.scope:
|
|
50
|
+
return []
|
|
51
|
+
out = []
|
|
52
|
+
for term, item in self.items.items():
|
|
53
|
+
other = item.get("confused_with", "").strip()
|
|
54
|
+
if not other:
|
|
55
|
+
continue
|
|
56
|
+
out.append({
|
|
57
|
+
"term": term,
|
|
58
|
+
"question": f"「{term}」是{other}吗?",
|
|
59
|
+
"answer": f"不是。{item.get('one_line', '')}",
|
|
60
|
+
"reason": item.get("reason", ""),
|
|
61
|
+
})
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load(path: str | Path | None) -> Disambiguation:
|
|
66
|
+
if not path:
|
|
67
|
+
return Disambiguation()
|
|
68
|
+
p = Path(path)
|
|
69
|
+
if not p.exists():
|
|
70
|
+
return Disambiguation()
|
|
71
|
+
return Disambiguation(json.loads(p.read_text(encoding="utf-8-sig")))
|