open-knowledge-studio 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {open_knowledge_studio-0.2.1/open_knowledge_studio.egg-info → open_knowledge_studio-0.2.2}/PKG-INFO +1 -1
- open_knowledge_studio-0.2.2/knowledge_studio/_assets/claude/hooks/user-prompt-recall.py +130 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/config.py +0 -26
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/distiller.py +2 -3
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/store.py +19 -16
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2/open_knowledge_studio.egg-info}/PKG-INFO +1 -1
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/open_knowledge_studio.egg-info/SOURCES.txt +0 -1
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/pyproject.toml +1 -1
- open_knowledge_studio-0.2.1/knowledge_studio/_assets/claude/hooks/user-prompt-recall.py +0 -82
- open_knowledge_studio-0.2.1/knowledge_studio/migrate.py +0 -183
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/LICENSE +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/MANIFEST.in +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/README.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/__init__.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/_meta/frontmatter-schema.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/_meta/learning-schema.json +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/_meta/raw-evidence-schema.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/hooks/pre-compact.sh +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/hooks/session-start.sh +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/hooks/user-prompt-recall.sh +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/hooks/validate-wiki-write.sh +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/rules/raw-intake.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/rules/wiki-writing.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/settings.json +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/archive/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/compile/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/ingest/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/lint/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/promote/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/query/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/start/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/claude/skills/status/SKILL.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/settings/handlers.json +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/settings/input-sources.json +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/settings/raw-tools.example.json +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/templates/anti-pattern.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/templates/concept.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/templates/draft.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/_assets/templates/strategy.md +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/cli.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/health.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/metrics.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/knowledge_studio/recall.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/open_knowledge_studio.egg-info/dependency_links.txt +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/open_knowledge_studio.egg-info/entry_points.txt +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/open_knowledge_studio.egg-info/requires.txt +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/open_knowledge_studio.egg-info/top_level.txt +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/setup.cfg +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/tests/test_distiller.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/tests/test_health.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/tests/test_init.py +0 -0
- {open_knowledge_studio-0.2.1 → open_knowledge_studio-0.2.2}/tests/test_recall.py +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""UserPromptSubmit hook — auto-recall relevant memory and inject it as context.
|
|
3
|
+
|
|
4
|
+
Reads the editor's UserPromptSubmit JSON payload on stdin (Claude Code and Qoder
|
|
5
|
+
share the same `.prompt` contract), runs the OKS recall engine against the user's
|
|
6
|
+
prompt, and prints a compact <recalled-memory> block on stdout (which the editor
|
|
7
|
+
adds to the model's context). Fails open: any error or empty result prints nothing
|
|
8
|
+
and exits 0, so it never blocks a prompt.
|
|
9
|
+
|
|
10
|
+
Tunables via env:
|
|
11
|
+
OKS_RECALL_FLOOR min knowledge relevance to inject (default 0.7)
|
|
12
|
+
OKS_RECALL_TOPN max memories injected (default 3)
|
|
13
|
+
OKS_RECALL_MINLEN skip prompts shorter than this many chars (default 6)
|
|
14
|
+
OKS_RECALL_COOLDOWN turns before the same slug may be re-injected (default 10)
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
_TRIVIAL = {
|
|
24
|
+
"你好", "谢谢", "多谢", "ok", "okay", "好", "好的", "嗯", "行", "继续",
|
|
25
|
+
"hi", "hello", "thanks", "thx", "yes", "no", "是", "对", "收到",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_payload() -> dict:
|
|
30
|
+
try:
|
|
31
|
+
payload = json.load(sys.stdin)
|
|
32
|
+
except Exception:
|
|
33
|
+
return {}
|
|
34
|
+
return payload if isinstance(payload, dict) else {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _state_path(session_id: str) -> Path:
|
|
38
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "_", session_id)[:80] or "default"
|
|
39
|
+
return Path(tempfile.gettempdir()) / f"oks-recall-{safe}.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_state(path: Path) -> dict:
|
|
43
|
+
try:
|
|
44
|
+
state = json.loads(path.read_text(encoding="utf-8"))
|
|
45
|
+
if isinstance(state, dict):
|
|
46
|
+
return {"n": int(state.get("n", 0)), "seen": dict(state.get("seen", {}))}
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return {"n": 0, "seen": {}}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _save_state(path: Path, state: dict) -> None:
|
|
53
|
+
try:
|
|
54
|
+
path.write_text(json.dumps(state), encoding="utf-8")
|
|
55
|
+
except Exception:
|
|
56
|
+
pass # state is best-effort; dedup degrades gracefully
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
payload = _load_payload()
|
|
61
|
+
prompt = str(payload.get("prompt", "") or "").strip()
|
|
62
|
+
if not prompt:
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
minlen = int(os.environ.get("OKS_RECALL_MINLEN", "6"))
|
|
66
|
+
if len(prompt) < minlen or prompt.lower() in _TRIVIAL:
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
from knowledge_studio.recall import recall
|
|
71
|
+
except Exception:
|
|
72
|
+
return 0 # engine unavailable — fail open
|
|
73
|
+
|
|
74
|
+
floor = float(os.environ.get("OKS_RECALL_FLOOR", "0.7"))
|
|
75
|
+
topn = int(os.environ.get("OKS_RECALL_TOPN", "3"))
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
hits = recall(query=prompt, limit=max(topn, 5)).get("knowledge", [])
|
|
79
|
+
except Exception:
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
cooldown = int(os.environ.get("OKS_RECALL_COOLDOWN", "10"))
|
|
83
|
+
session_id = str(payload.get("session_id", "") or "")
|
|
84
|
+
state_file = _state_path(session_id)
|
|
85
|
+
state = _load_state(state_file)
|
|
86
|
+
state["n"] += 1
|
|
87
|
+
turn = state["n"]
|
|
88
|
+
|
|
89
|
+
picked = []
|
|
90
|
+
for h in hits:
|
|
91
|
+
if float(h.get("relevance", 0)) < floor:
|
|
92
|
+
continue
|
|
93
|
+
slug = str(h.get("slug", "")).strip()
|
|
94
|
+
last = state["seen"].get(slug)
|
|
95
|
+
if slug and last is not None and turn - int(last) < cooldown:
|
|
96
|
+
continue
|
|
97
|
+
picked.append(h)
|
|
98
|
+
if len(picked) >= topn:
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
if not picked:
|
|
102
|
+
_save_state(state_file, state)
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
for h in picked:
|
|
106
|
+
slug = str(h.get("slug", "")).strip()
|
|
107
|
+
if slug:
|
|
108
|
+
state["seen"][slug] = turn
|
|
109
|
+
_save_state(state_file, state)
|
|
110
|
+
|
|
111
|
+
lines = [
|
|
112
|
+
"<recalled-memory source=\"oks\">",
|
|
113
|
+
"相关已沉淀记忆(引用时用 slug;与当前事实冲突以最新为准):",
|
|
114
|
+
]
|
|
115
|
+
for h in picked:
|
|
116
|
+
title = str(h.get("title", h.get("slug", ""))).strip()
|
|
117
|
+
slug = str(h.get("slug", "")).strip()
|
|
118
|
+
htype = str(h.get("type", "")).strip()
|
|
119
|
+
rel = float(h.get("relevance", 0))
|
|
120
|
+
preview = re.sub(r"\s+", " ", str(h.get("body_preview", ""))).strip()[:160]
|
|
121
|
+
lines.append(f"- [{htype}] {title} ({slug}) rel={rel:.2f}")
|
|
122
|
+
if preview:
|
|
123
|
+
lines.append(f" {preview}")
|
|
124
|
+
lines.append("</recalled-memory>")
|
|
125
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
sys.exit(main())
|
|
@@ -116,29 +116,3 @@ def get_kb_root() -> Path:
|
|
|
116
116
|
return Path(kb_path)
|
|
117
117
|
|
|
118
118
|
return Path.cwd()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def get_handler_config(handler_name: str) -> dict[str, Any]:
|
|
122
|
-
"""Get handler-specific config from global config."""
|
|
123
|
-
config = load_config()
|
|
124
|
-
return config.get("handlers", {}).get(handler_name, {})
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def get_api_key(provider: str = "openai") -> str:
|
|
128
|
-
"""Get API key for a provider.
|
|
129
|
-
|
|
130
|
-
Priority:
|
|
131
|
-
1. Environment variable (OPENAI_API_KEY, ANTHROPIC_API_KEY)
|
|
132
|
-
2. ~/.oks/config.json → api_keys.<provider>
|
|
133
|
-
"""
|
|
134
|
-
env_map = {
|
|
135
|
-
"openai": "OPENAI_API_KEY",
|
|
136
|
-
"anthropic": "ANTHROPIC_API_KEY",
|
|
137
|
-
}
|
|
138
|
-
env_var = env_map.get(provider, f"{provider.upper()}_API_KEY")
|
|
139
|
-
env_val = os.environ.get(env_var)
|
|
140
|
-
if env_val:
|
|
141
|
-
return env_val
|
|
142
|
-
|
|
143
|
-
config = load_config()
|
|
144
|
-
return config.get("api_keys", {}).get(provider, "")
|
|
@@ -27,6 +27,7 @@ from knowledge_studio.store import (
|
|
|
27
27
|
apply_decay,
|
|
28
28
|
compute_score,
|
|
29
29
|
drafts_dir,
|
|
30
|
+
make_slug,
|
|
30
31
|
parse_wiki_file,
|
|
31
32
|
wiki_dir,
|
|
32
33
|
)
|
|
@@ -50,9 +51,7 @@ def write_digest(
|
|
|
50
51
|
date_dir = rd / now.strftime("%Y") / now.strftime("%m") / now.strftime("%d")
|
|
51
52
|
date_dir.mkdir(parents=True, exist_ok=True)
|
|
52
53
|
|
|
53
|
-
slug =
|
|
54
|
-
if not slug:
|
|
55
|
-
slug = "digest"
|
|
54
|
+
slug = make_slug(title, fallback="digest")
|
|
56
55
|
slug = f"{now.strftime('%Y%m%d')}-{slug}"
|
|
57
56
|
|
|
58
57
|
file_path = date_dir / f"{slug}.md"
|
|
@@ -218,23 +218,25 @@ def compute_tier(score: float) -> str:
|
|
|
218
218
|
|
|
219
219
|
|
|
220
220
|
def compute_quality(meta: dict) -> int:
|
|
221
|
+
# Content factors (55) are reachable by any well-written page;
|
|
222
|
+
# traces/review (40) are earned bonuses, not the baseline.
|
|
221
223
|
score = 0
|
|
222
|
-
if meta.get("traces"):
|
|
223
|
-
score += 25
|
|
224
|
-
if meta.get("review"):
|
|
225
|
-
score += 25
|
|
226
|
-
if meta.get("options"):
|
|
227
|
-
score += 15
|
|
228
224
|
if len(meta.get("body", "")) >= 50:
|
|
229
|
-
score +=
|
|
225
|
+
score += 25
|
|
230
226
|
if meta.get("importance", 0) >= 0.7:
|
|
231
|
-
score +=
|
|
227
|
+
score += 15
|
|
232
228
|
tags = meta.get("tags", "")
|
|
233
229
|
if isinstance(tags, list):
|
|
234
230
|
if tags:
|
|
235
|
-
score +=
|
|
231
|
+
score += 15
|
|
236
232
|
elif isinstance(tags, str) and tags.strip():
|
|
237
|
-
score +=
|
|
233
|
+
score += 15
|
|
234
|
+
if meta.get("traces"):
|
|
235
|
+
score += 20
|
|
236
|
+
if meta.get("review"):
|
|
237
|
+
score += 20
|
|
238
|
+
if meta.get("options"):
|
|
239
|
+
score += 5
|
|
238
240
|
return score
|
|
239
241
|
|
|
240
242
|
|
|
@@ -326,9 +328,6 @@ def list_wiki_pages(config: dict | None = None) -> list[dict]:
|
|
|
326
328
|
meta["quality_score"] = compute_quality(meta)
|
|
327
329
|
if "status" not in meta:
|
|
328
330
|
meta["status"] = "active"
|
|
329
|
-
if "type" in meta and "category" not in meta:
|
|
330
|
-
type_to_cat = {"concept": "insight", "strategy": "pattern", "anti-pattern": "mistake"}
|
|
331
|
-
meta["category"] = type_to_cat.get(meta["type"], meta["type"])
|
|
332
331
|
pages.append(meta)
|
|
333
332
|
|
|
334
333
|
pages.sort(key=lambda x: (-x["score"], x["slug"]))
|
|
@@ -358,6 +357,12 @@ def record_access(slug: str) -> None:
|
|
|
358
357
|
_reinforce_confidence(slug)
|
|
359
358
|
|
|
360
359
|
|
|
360
|
+
def make_slug(title: str, fallback: str = "untitled") -> str:
|
|
361
|
+
# Keep CJK characters so Chinese titles don't degrade to the fallback.
|
|
362
|
+
slug = re.sub(r"[^a-z0-9\u4e00-\u9fff]+", "-", title.lower())[:60].strip("-")
|
|
363
|
+
return slug or fallback
|
|
364
|
+
|
|
365
|
+
|
|
361
366
|
def write_wiki_page(
|
|
362
367
|
title: str,
|
|
363
368
|
content: str,
|
|
@@ -389,9 +394,7 @@ def write_wiki_page(
|
|
|
389
394
|
type_dir = wd / area / wiki_type
|
|
390
395
|
type_dir.mkdir(parents=True, exist_ok=True)
|
|
391
396
|
|
|
392
|
-
slug =
|
|
393
|
-
if not slug:
|
|
394
|
-
slug = "untitled"
|
|
397
|
+
slug = make_slug(title, fallback="untitled")
|
|
395
398
|
slug = f"{date_str}-{slug}"
|
|
396
399
|
|
|
397
400
|
file_path = type_dir / f"{slug}.md"
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""UserPromptSubmit hook — auto-recall relevant memory and inject it as context.
|
|
3
|
-
|
|
4
|
-
Reads the editor's UserPromptSubmit JSON payload on stdin (Claude Code and Qoder
|
|
5
|
-
share the same `.prompt` contract), runs the OKS recall engine against the user's
|
|
6
|
-
prompt, and prints a compact <recalled-memory> block on stdout (which the editor
|
|
7
|
-
adds to the model's context). Fails open: any error or empty result prints nothing
|
|
8
|
-
and exits 0, so it never blocks a prompt.
|
|
9
|
-
|
|
10
|
-
Tunables via env:
|
|
11
|
-
OKS_RECALL_FLOOR min knowledge relevance to inject (default 0.7)
|
|
12
|
-
OKS_RECALL_TOPN max memories injected (default 3)
|
|
13
|
-
OKS_RECALL_MINLEN skip prompts shorter than this many chars (default 6)
|
|
14
|
-
"""
|
|
15
|
-
import json
|
|
16
|
-
import os
|
|
17
|
-
import re
|
|
18
|
-
import sys
|
|
19
|
-
|
|
20
|
-
_TRIVIAL = {
|
|
21
|
-
"你好", "谢谢", "多谢", "ok", "okay", "好", "好的", "嗯", "行", "继续",
|
|
22
|
-
"hi", "hello", "thanks", "thx", "yes", "no", "是", "对", "收到",
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _load_prompt() -> str:
|
|
27
|
-
try:
|
|
28
|
-
payload = json.load(sys.stdin)
|
|
29
|
-
except Exception:
|
|
30
|
-
return ""
|
|
31
|
-
if isinstance(payload, dict):
|
|
32
|
-
return str(payload.get("prompt", "") or "")
|
|
33
|
-
return ""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def main() -> int:
|
|
37
|
-
prompt = _load_prompt().strip()
|
|
38
|
-
if not prompt:
|
|
39
|
-
return 0
|
|
40
|
-
|
|
41
|
-
minlen = int(os.environ.get("OKS_RECALL_MINLEN", "6"))
|
|
42
|
-
if len(prompt) < minlen or prompt.lower() in _TRIVIAL:
|
|
43
|
-
return 0
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
from knowledge_studio.recall import recall
|
|
47
|
-
except Exception:
|
|
48
|
-
return 0 # engine unavailable — fail open
|
|
49
|
-
|
|
50
|
-
floor = float(os.environ.get("OKS_RECALL_FLOOR", "0.7"))
|
|
51
|
-
topn = int(os.environ.get("OKS_RECALL_TOPN", "3"))
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
hits = recall(query=prompt, limit=max(topn, 5)).get("knowledge", [])
|
|
55
|
-
except Exception:
|
|
56
|
-
return 0
|
|
57
|
-
|
|
58
|
-
picked = [h for h in hits if float(h.get("relevance", 0)) >= floor][:topn]
|
|
59
|
-
if not picked:
|
|
60
|
-
return 0
|
|
61
|
-
|
|
62
|
-
lines = [
|
|
63
|
-
"<recalled-memory source=\"oks\">",
|
|
64
|
-
"知识库中与本次输入相关的已沉淀记忆(按相关度排序)。回答时优先参考并按 slug 引用;"
|
|
65
|
-
"若与当前事实冲突以最新为准;学到值得留存的新知识时用 oks 写入 drafts/wiki。",
|
|
66
|
-
]
|
|
67
|
-
for h in picked:
|
|
68
|
-
title = str(h.get("title", h.get("slug", ""))).strip()
|
|
69
|
-
slug = str(h.get("slug", "")).strip()
|
|
70
|
-
htype = str(h.get("type", "")).strip()
|
|
71
|
-
rel = float(h.get("relevance", 0))
|
|
72
|
-
preview = re.sub(r"\s+", " ", str(h.get("body_preview", ""))).strip()[:160]
|
|
73
|
-
lines.append(f"- [{htype}] {title} ({slug}) rel={rel:.2f}")
|
|
74
|
-
if preview:
|
|
75
|
-
lines.append(f" {preview}")
|
|
76
|
-
lines.append("</recalled-memory>")
|
|
77
|
-
sys.stdout.write("\n".join(lines) + "\n")
|
|
78
|
-
return 0
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if __name__ == "__main__":
|
|
82
|
-
sys.exit(main())
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
"""Domain → Wiki migration — one-time conversion.
|
|
2
|
-
|
|
3
|
-
Extracted from autpilot-web/backend/app/services/migrate_domain_to_wiki.py.
|
|
4
|
-
Removed settings/knowledge_sync dependencies. Uses store._atomic_write.
|
|
5
|
-
|
|
6
|
-
Migration mapping:
|
|
7
|
-
domain/{area}/foundations/*.md -> wiki/{area}/concepts/ (type: concept)
|
|
8
|
-
domain/{area}/conventions/*.md -> wiki/{area}/strategies/ (type: strategy)
|
|
9
|
-
domain/{area}/context/*.md -> wiki/{area}/concepts/ (type: concept)
|
|
10
|
-
domain/{area}/strategies/**/*.md -> wiki/{area}/strategies/ (type: strategy)
|
|
11
|
-
domain/_shared/**/*.md -> wiki/_shared/concepts/ (type: concept)
|
|
12
|
-
|
|
13
|
-
Idempotent: if the target wiki/ file already exists, it is skipped.
|
|
14
|
-
Original domain/ files are NOT deleted.
|
|
15
|
-
"""
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import logging
|
|
19
|
-
from datetime import UTC, datetime
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
import yaml
|
|
23
|
-
|
|
24
|
-
from knowledge_studio.store import _atomic_write, repo_root
|
|
25
|
-
|
|
26
|
-
_logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
_SUBDIR_MAPPINGS: list[tuple[str, str, str]] = [
|
|
29
|
-
("foundations", "concepts", "concept"),
|
|
30
|
-
("conventions", "strategies", "strategy"),
|
|
31
|
-
("context", "concepts", "concept"),
|
|
32
|
-
("strategies", "strategies", "strategy"),
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _domain_dir() -> Path:
|
|
37
|
-
return repo_root() / "domain"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _wiki_dir() -> Path:
|
|
41
|
-
return repo_root() / "wiki"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _resolve_migration(
|
|
45
|
-
src_path: Path, domain_root: Path,
|
|
46
|
-
) -> tuple[Path, str] | None:
|
|
47
|
-
"""Resolve the target wiki path and wiki type for a domain source file.
|
|
48
|
-
|
|
49
|
-
Returns (target_path, wiki_type) or None if no mapping rule applies.
|
|
50
|
-
"""
|
|
51
|
-
try:
|
|
52
|
-
rel = src_path.relative_to(domain_root)
|
|
53
|
-
except ValueError:
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
parts = rel.parts
|
|
57
|
-
if not parts:
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
if parts[0] == "_shared":
|
|
61
|
-
target = _wiki_dir() / "_shared" / "concepts" / src_path.name
|
|
62
|
-
return target, "concept"
|
|
63
|
-
|
|
64
|
-
if len(parts) < 2:
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
area = parts[0]
|
|
68
|
-
src_subdir = parts[1]
|
|
69
|
-
|
|
70
|
-
for subdir, target_subdir, wiki_type in _SUBDIR_MAPPINGS:
|
|
71
|
-
if src_subdir == subdir:
|
|
72
|
-
target = _wiki_dir() / area / target_subdir / src_path.name
|
|
73
|
-
return target, wiki_type
|
|
74
|
-
|
|
75
|
-
return None
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
|
79
|
-
"""Parse a Markdown file with YAML frontmatter.
|
|
80
|
-
|
|
81
|
-
Returns (meta_dict, body_string). If no valid frontmatter is found,
|
|
82
|
-
returns ({}, full_text).
|
|
83
|
-
"""
|
|
84
|
-
if not text.startswith("---"):
|
|
85
|
-
return {}, text
|
|
86
|
-
|
|
87
|
-
split = text.split("---", 2)
|
|
88
|
-
if len(split) < 3:
|
|
89
|
-
return {}, text
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
meta = yaml.safe_load(split[1].strip()) or {}
|
|
93
|
-
except yaml.YAMLError:
|
|
94
|
-
return {}, text
|
|
95
|
-
|
|
96
|
-
if not isinstance(meta, dict):
|
|
97
|
-
return {}, text
|
|
98
|
-
|
|
99
|
-
return meta, split[2].lstrip("\n")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _build_frontmatter(meta: dict, wiki_type: str) -> str:
|
|
103
|
-
"""Build migrated frontmatter preserving existing fields and setting required ones."""
|
|
104
|
-
today = datetime.now(UTC).strftime("%Y-%m-%d")
|
|
105
|
-
|
|
106
|
-
fm: dict = {}
|
|
107
|
-
|
|
108
|
-
for key in ("title", "tags", "area", "source", "last_verified"):
|
|
109
|
-
if key in meta:
|
|
110
|
-
fm[key] = meta[key]
|
|
111
|
-
|
|
112
|
-
fm["type"] = wiki_type
|
|
113
|
-
fm["status"] = "active"
|
|
114
|
-
fm["importance"] = 0.7
|
|
115
|
-
fm["confidence"] = meta.get("confidence", 0.8)
|
|
116
|
-
fm["created"] = meta.get("created", today)
|
|
117
|
-
fm["pinned"] = False
|
|
118
|
-
fm["archived"] = False
|
|
119
|
-
fm["access_count"] = 0
|
|
120
|
-
|
|
121
|
-
return yaml.dump(
|
|
122
|
-
fm,
|
|
123
|
-
default_flow_style=False,
|
|
124
|
-
allow_unicode=True,
|
|
125
|
-
sort_keys=False,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def run_migration() -> dict:
|
|
130
|
-
"""Run the domain -> wiki migration.
|
|
131
|
-
|
|
132
|
-
Walks ``domain/`` recursively, converts each matching ``.md`` file to
|
|
133
|
-
the new ``wiki/`` layout with updated frontmatter, and writes it
|
|
134
|
-
atomically.
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
``{"migrated": int, "skipped": int, "errors": list[str]}``
|
|
138
|
-
|
|
139
|
-
The migration is **idempotent**: if a target wiki/ file already exists,
|
|
140
|
-
it is skipped. Original ``domain/`` files are never deleted.
|
|
141
|
-
"""
|
|
142
|
-
domain_root = _domain_dir()
|
|
143
|
-
if not domain_root.exists():
|
|
144
|
-
return {
|
|
145
|
-
"migrated": 0,
|
|
146
|
-
"skipped": 0,
|
|
147
|
-
"errors": ["domain/ directory not found"],
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
migrated = 0
|
|
151
|
-
skipped = 0
|
|
152
|
-
errors: list[str] = []
|
|
153
|
-
|
|
154
|
-
for src_path in sorted(domain_root.rglob("*.md")):
|
|
155
|
-
resolved = _resolve_migration(src_path, domain_root)
|
|
156
|
-
if resolved is None:
|
|
157
|
-
continue
|
|
158
|
-
|
|
159
|
-
target_path, wiki_type = resolved
|
|
160
|
-
|
|
161
|
-
if target_path.exists():
|
|
162
|
-
skipped += 1
|
|
163
|
-
continue
|
|
164
|
-
|
|
165
|
-
try:
|
|
166
|
-
text = src_path.read_text(encoding="utf-8")
|
|
167
|
-
meta, body = _parse_frontmatter(text)
|
|
168
|
-
|
|
169
|
-
frontmatter_str = _build_frontmatter(meta, wiki_type)
|
|
170
|
-
content = f"---\n{frontmatter_str}---\n\n{body}"
|
|
171
|
-
|
|
172
|
-
_atomic_write(target_path, content)
|
|
173
|
-
migrated += 1
|
|
174
|
-
_logger.info("Migrated %s -> %s", src_path, target_path)
|
|
175
|
-
except Exception as exc:
|
|
176
|
-
errors.append(f"{src_path}: {exc}")
|
|
177
|
-
_logger.warning("Migration error for %s: %s", src_path, exc)
|
|
178
|
-
|
|
179
|
-
_logger.info(
|
|
180
|
-
"domain->wiki migration complete: %d migrated, %d skipped, %d errors",
|
|
181
|
-
migrated, skipped, len(errors),
|
|
182
|
-
)
|
|
183
|
-
return {"migrated": migrated, "skipped": skipped, "errors": errors}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|