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/__init__.py
ADDED
mcai/__main__.py
ADDED
mcai/autopilot.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""自动驾驶:让知识库在无人参与下保持「活着、准着、可被调用」。
|
|
2
|
+
|
|
3
|
+
一轮循环做四件事:
|
|
4
|
+
1. 检查语料/配置指纹,变了就重建;
|
|
5
|
+
2. 记录变更日志(新增/删除/改写的术语),让每次变化可追溯;
|
|
6
|
+
3. 健康自检(知识库、站点、服务端口、接口响应);
|
|
7
|
+
4. 把状态写到 dist/status.json 与 data/status.json,供调用方判断是否健康。
|
|
8
|
+
|
|
9
|
+
设计为「一次性执行」:由系统计划任务每 5 分钟调用一次,比常驻进程更抗崩溃。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import socket
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import urllib.request
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from . import dashboard, knowledge, pipeline, quality, selftest, snapshot
|
|
24
|
+
|
|
25
|
+
DEFAULT_PORT = 8787
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Autopilot:
|
|
29
|
+
def __init__(self, root: str | Path, port: int = DEFAULT_PORT, ensure_server: bool = True):
|
|
30
|
+
self.root = Path(root).resolve()
|
|
31
|
+
self.port = port
|
|
32
|
+
self.ensure_server = ensure_server
|
|
33
|
+
self.data = self.root / "data"
|
|
34
|
+
self.data.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
# ---------- 指纹 ----------
|
|
37
|
+
def fingerprint(self) -> str:
|
|
38
|
+
h = hashlib.sha256()
|
|
39
|
+
cfg_path = self.root / "config" / "corpus.json"
|
|
40
|
+
for path in sorted((self.root / "config").glob("*.json")):
|
|
41
|
+
h.update(path.name.encode("utf-8"))
|
|
42
|
+
h.update(path.read_bytes())
|
|
43
|
+
try:
|
|
44
|
+
cfg = json.loads(cfg_path.read_text(encoding="utf-8-sig"))
|
|
45
|
+
except Exception: # noqa: BLE001
|
|
46
|
+
cfg = {}
|
|
47
|
+
for spec in cfg.get("corpus", []):
|
|
48
|
+
raw = Path(spec.get("path", ""))
|
|
49
|
+
path = raw if raw.is_absolute() else self.root / raw
|
|
50
|
+
if path.exists():
|
|
51
|
+
stat = path.stat()
|
|
52
|
+
h.update(f"{spec.get('id')}:{stat.st_size}:{int(stat.st_mtime)}".encode("utf-8"))
|
|
53
|
+
else:
|
|
54
|
+
h.update(f"{spec.get('id')}:missing".encode("utf-8"))
|
|
55
|
+
return h.hexdigest()
|
|
56
|
+
|
|
57
|
+
# ---------- 快照与差异 ----------
|
|
58
|
+
def _snapshot(self) -> dict:
|
|
59
|
+
path = self.data / "kb.json"
|
|
60
|
+
if not path.exists():
|
|
61
|
+
return {}
|
|
62
|
+
try:
|
|
63
|
+
kb = json.loads(path.read_text(encoding="utf-8"))
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
return {}
|
|
66
|
+
return {
|
|
67
|
+
c["term"]: hashlib.md5(c.get("definition", "").encode("utf-8")).hexdigest()
|
|
68
|
+
for c in kb.get("concepts", [])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _diff(before: dict, after: dict) -> dict:
|
|
73
|
+
added = sorted(set(after) - set(before))
|
|
74
|
+
removed = sorted(set(before) - set(after))
|
|
75
|
+
changed = sorted(t for t in set(before) & set(after) if before[t] != after[t])
|
|
76
|
+
return {"added": added, "removed": removed, "changed": changed}
|
|
77
|
+
|
|
78
|
+
# ---------- 服务检查 ----------
|
|
79
|
+
def _port_open(self) -> bool:
|
|
80
|
+
try:
|
|
81
|
+
with socket.create_connection(("127.0.0.1", self.port), timeout=0.6):
|
|
82
|
+
return True
|
|
83
|
+
except OSError:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def _server_healthy(self) -> bool:
|
|
87
|
+
try:
|
|
88
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{self.port}/api/v1/health", timeout=2) as resp:
|
|
89
|
+
return resp.status == 200
|
|
90
|
+
except Exception: # noqa: BLE001
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def _start_server(self) -> bool:
|
|
94
|
+
if not self.ensure_server or self._port_open():
|
|
95
|
+
return False
|
|
96
|
+
flags = 0
|
|
97
|
+
if sys.platform == "win32":
|
|
98
|
+
flags = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
|
99
|
+
try:
|
|
100
|
+
subprocess.Popen(
|
|
101
|
+
[sys.executable, "-m", "mcai", "serve", "--home", str(self.root), "--port", str(self.port)],
|
|
102
|
+
cwd=str(self.root),
|
|
103
|
+
stdin=subprocess.DEVNULL,
|
|
104
|
+
stdout=subprocess.DEVNULL,
|
|
105
|
+
stderr=subprocess.DEVNULL,
|
|
106
|
+
creationflags=flags,
|
|
107
|
+
)
|
|
108
|
+
for _ in range(10):
|
|
109
|
+
time.sleep(0.3)
|
|
110
|
+
if self._port_open():
|
|
111
|
+
return True
|
|
112
|
+
except Exception: # noqa: BLE001
|
|
113
|
+
return False
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
# ---------- 主循环 ----------
|
|
117
|
+
def run_once(self) -> dict:
|
|
118
|
+
started = time.time()
|
|
119
|
+
state_path = self.data / "autopilot-state.json"
|
|
120
|
+
state = {}
|
|
121
|
+
if state_path.exists():
|
|
122
|
+
try:
|
|
123
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
state = {}
|
|
126
|
+
|
|
127
|
+
before = self._snapshot()
|
|
128
|
+
fingerprint = self.fingerprint()
|
|
129
|
+
rebuilt = False
|
|
130
|
+
error = ""
|
|
131
|
+
result = {}
|
|
132
|
+
|
|
133
|
+
needs_build = (fingerprint != state.get("fingerprint")) or not (self.data / "kb.json").exists()
|
|
134
|
+
if needs_build:
|
|
135
|
+
try:
|
|
136
|
+
result = pipeline.rebuild(self.root)
|
|
137
|
+
rebuilt = True
|
|
138
|
+
except Exception as exc: # noqa: BLE001
|
|
139
|
+
error = f"{type(exc).__name__}: {exc}"
|
|
140
|
+
|
|
141
|
+
after = self._snapshot()
|
|
142
|
+
diff = self._diff(before, after) if rebuilt else {"added": [], "removed": [], "changed": []}
|
|
143
|
+
|
|
144
|
+
started_server = self._start_server()
|
|
145
|
+
server_ok = self._server_healthy()
|
|
146
|
+
|
|
147
|
+
kb = {}
|
|
148
|
+
try:
|
|
149
|
+
kb = knowledge.load(self.root).meta.get("stats", {})
|
|
150
|
+
except Exception: # noqa: BLE001
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
q = quality.inspect(self.root)
|
|
155
|
+
except Exception as exc: # noqa: BLE001
|
|
156
|
+
q = {"ok": False, "counts": {"error": 1, "warn": 0},
|
|
157
|
+
"errors": [{"detail": f"{type(exc).__name__}: {exc}"}]}
|
|
158
|
+
try:
|
|
159
|
+
t = selftest.run(self.root)
|
|
160
|
+
except Exception as exc: # noqa: BLE001
|
|
161
|
+
t = {"ok": False, "passed": 0, "total": 0, "failed": [f"{type(exc).__name__}: {exc}"]}
|
|
162
|
+
|
|
163
|
+
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
164
|
+
status = {
|
|
165
|
+
"ok": (not error and bool(kb) and (server_ok if self.ensure_server else True)
|
|
166
|
+
and q.get("ok", False) and t.get("ok", False)),
|
|
167
|
+
"quality": {"ok": q.get("ok"), "errors": q.get("counts", {}).get("error", 0),
|
|
168
|
+
"warnings": q.get("counts", {}).get("warn", 0)},
|
|
169
|
+
"selftest": {"ok": t.get("ok"), "passed": t.get("passed"), "total": t.get("total"),
|
|
170
|
+
"failed": t.get("failed", [])},
|
|
171
|
+
"checked_at": now,
|
|
172
|
+
"built_at": result.get("built_at") or kb.get("built_at"),
|
|
173
|
+
"rebuilt": rebuilt,
|
|
174
|
+
"started_server": started_server,
|
|
175
|
+
"server_ok": server_ok,
|
|
176
|
+
"concepts": kb.get("concepts"),
|
|
177
|
+
"faqs": kb.get("faqs"),
|
|
178
|
+
"relations": kb.get("relations"),
|
|
179
|
+
"evidence": kb.get("evidence"),
|
|
180
|
+
"error": error,
|
|
181
|
+
"duration_ms": int((time.time() - started) * 1000),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# 写状态:data 里永久保留,dist 里给外部读(重建会清空 dist,所以最后写)
|
|
185
|
+
(self.data / "status.json").write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
186
|
+
dist = self.root / "dist"
|
|
187
|
+
if dist.exists():
|
|
188
|
+
(dist / "status.json").write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
189
|
+
|
|
190
|
+
# 变更日志
|
|
191
|
+
if rebuilt and before and (diff["added"] or diff["removed"] or diff["changed"]):
|
|
192
|
+
entry = {"ts": now, **diff, "concepts": status["concepts"], "faqs": status["faqs"]}
|
|
193
|
+
with (self.data / "changelog.jsonl").open("a", encoding="utf-8") as fh:
|
|
194
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
195
|
+
|
|
196
|
+
with (self.data / "autopilot.jsonl").open("a", encoding="utf-8") as fh:
|
|
197
|
+
fh.write(json.dumps(status, ensure_ascii=False) + "\n")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
dashboard.render(self.root)
|
|
201
|
+
except Exception: # noqa: BLE001 - 看板失败不影响巡检
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
if rebuilt:
|
|
205
|
+
try:
|
|
206
|
+
snap = snapshot.create(self.root, note="自动快照:语料变更", keep=8)
|
|
207
|
+
status["snapshot"] = Path(snap["archive"]).name
|
|
208
|
+
except Exception: # noqa: BLE001 - 快照失败不影响巡检
|
|
209
|
+
pass
|
|
210
|
+
state["fingerprint"] = fingerprint
|
|
211
|
+
state["last_build"] = now
|
|
212
|
+
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
213
|
+
|
|
214
|
+
return {"status": status, "diff": diff, "rebuilt": rebuilt}
|
|
215
|
+
|
|
216
|
+
def loop(self, interval: int = 60) -> None:
|
|
217
|
+
print(f"自动驾驶已启动(每 {interval} 秒检查一次,Ctrl+C 停止)")
|
|
218
|
+
while True:
|
|
219
|
+
try:
|
|
220
|
+
outcome = self.run_once()
|
|
221
|
+
s = outcome["status"]
|
|
222
|
+
flag = "重建" if outcome["rebuilt"] else "无变化"
|
|
223
|
+
print(f"[{s['checked_at']}] {flag} · {s['concepts']} 概念 · 服务 "
|
|
224
|
+
f"{'在线' if s['server_ok'] else '离线'} · {s['duration_ms']}ms")
|
|
225
|
+
except KeyboardInterrupt:
|
|
226
|
+
raise
|
|
227
|
+
except Exception as exc: # noqa: BLE001
|
|
228
|
+
print(f"[{datetime.now().isoformat(timespec='seconds')}] 异常:{type(exc).__name__}: {exc}")
|
|
229
|
+
time.sleep(max(5, interval))
|