Skip to content

Commit c1fe2a0

Browse files
feat(skill): distill an OpenKB wiki into a redistributable Anthropic Skill (#57)
* feat(skill): scaffold openkb/prompts/ for static system prompts
1 parent d4100c8 commit c1fe2a0

29 files changed

Lines changed: 8518 additions & 23 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ venv/
1313
raw/
1414
wiki/
1515
.openkb/
16+
output/
1617

1718
# Local only
1819
docs/

README.md

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,7 @@ The idea is based on a [concept](https://x.com/karpathy/status/20398056595256445
2222

2323
Traditional RAG rediscovers knowledge from scratch on every query. Nothing accumulates. OpenKB compiles knowledge once into a persistent wiki, then keeps it current. Cross-references already exist. Contradictions are flagged. Synthesis reflects everything consumed.
2424

25-
### Features
26-
27-
- **Broad format support** — PDF, Word, Markdown, PowerPoint, HTML, Excel, text, and more via markitdown
28-
- **Scale to long documents** — Long and complex documents are handled via [PageIndex](https://github.com/VectifyAI/PageIndex) tree indexing, enabling accurate, vectorless long-context retrieval
29-
- **Native multi-modality** — Retrieves and understands figures, tables, and images, not just text
30-
- **Compiled Wiki** — LLM manages and compiles your documents into summaries, concept pages, and cross-links, all kept in sync
31-
- **Query** — Ask questions (one-off) against your wiki. The LLM navigates your compiled knowledge to answer
32-
- **Interactive Chat** — Multi-turn conversations with persisted sessions you can resume across runs
33-
- **Lint** — Health checks find contradictions, gaps, orphans, and stale content
34-
- **Watch mode** — Drop files into `raw/`, wiki updates automatically
35-
- **Obsidian compatible** — Wiki is plain `.md` files with `[[wikilinks]]`. Open in Obsidian for graph view and browsing
25+
OpenKB has two layers: a **wiki foundation** that compiles and maintains your knowledge, and **generators** (query / chat / Skill Factory) that turn it into useful output. See [Usage](#️-usage) for the full command list.
3626

3727
# 🚀 Getting Started
3828

@@ -80,6 +70,9 @@ openkb query "What are the main findings?"
8070

8171
# 5. Or chat interactively
8272
openkb chat
73+
74+
# 6. Or distill your wiki into a redistributable skill
75+
openkb skill new my-expert "Reason like an expert on <topic-from-your-docs>"
8376
```
8477

8578
### Set up your LLM
@@ -109,7 +102,7 @@ raw/ You drop files here
109102
│ Wiki Compilation (using LLM)
110103
│ │
111104
▼ ▼
112-
wiki/
105+
wiki/ │ ← the foundation
113106
├── index.md Knowledge base overview
114107
├── log.md Operations timeline
115108
├── AGENTS.md Wiki schema (LLM instructions)
@@ -118,6 +111,13 @@ wiki/
118111
├── concepts/ Cross-document synthesis ← the good stuff
119112
├── explorations/ Saved query results
120113
└── reports/ Lint reports
114+
115+
┌──────────────────────┼──────────────────────┐
116+
▼ ▼ ▼
117+
query / chat Skill Factory (future)
118+
(LLM answers from openkb skill new ppt / podcast /
119+
the wiki) → output/skills/ report / …
120+
+ marketplace.json
121121
```
122122

123123
### Short vs. Long Document Handling
@@ -144,15 +144,15 @@ A single source might touch 10-15 wiki pages. Knowledge accumulates: each docume
144144

145145
# ⚙️ Usage
146146

147-
### Commands
147+
OpenKB commands fall into two layers: the **wiki foundation** (compile + manage your knowledge) and **generators** (turn that wiki into useful output).
148+
149+
## 🧱 Wiki Foundation — compile and maintain
148150

149151
| Command | Description |
150152
|---|---|
151153
| `openkb init` | Initialize a new knowledge base (interactive) |
152154
| <code>openkb&nbsp;add&nbsp;&lt;file_or_dir_or_URL&gt;</code> | Add documents and compile to wiki. URL ingest auto-detects PDF (saved as `.pdf` → PageIndex / markitdown) vs HTML (trafilatura main-content extract → `.md`) |
153155
| <code>openkb&nbsp;remove&nbsp;&lt;doc&gt;</code> | Remove a document and clean up its wiki pages, images, registry, and PageIndex state (use `--dry-run` to preview, `--keep-raw` / `--keep-empty-concepts` to retain artifacts) |
154-
| <code>openkb&nbsp;query&nbsp;"question"</code> | Ask a question over the knowledge base (use `--save` to save the answer to `wiki/explorations/`) |
155-
| `openkb chat` | Start an interactive multi-turn chat (use `--resume`, `--list`, `--delete` to manage sessions) |
156156
| `openkb watch` | Watch `raw/` and auto-compile new files |
157157
| `openkb lint` | Run structural + knowledge health checks |
158158
| `openkb list` | List indexed documents and concepts |
@@ -161,11 +161,26 @@ A single source might touch 10-15 wiki pages. Knowledge accumulates: each docume
161161

162162
<!-- | `openkb lint --fix` | Auto-fix what it can | -->
163163

164-
### Interactive Chat
164+
## ✨ Generators — turn the wiki into output
165+
166+
A "generator" reads from the compiled wiki and produces something usable: an answer, a conversation, a skill folder. The wiki is the substrate; generators are the surfaces.
167+
168+
| Command | Output |
169+
|---|---|
170+
| <code>openkb&nbsp;query&nbsp;"question"</code> | A grounded answer with citations (use `--save` to persist to `wiki/explorations/`) |
171+
| `openkb chat` | Interactive multi-turn session over the wiki (use `--resume`, `--list`, `--delete` to manage sessions) |
172+
| <code>openkb&nbsp;skill&nbsp;new&nbsp;&lt;name&gt;&nbsp;"&lt;intent&gt;"</code> | A redistributable Anthropic Skill at `<kb>/output/skills/<name>/` + auto-updated `marketplace.json` |
173+
| <code>openkb&nbsp;skill&nbsp;validate&nbsp;[name]</code> | Structural lint of compiled skills (frontmatter, file sizes, wikilinks, scripts/ stdlib check with `--strict`). Auto-runs at end of `skill new` |
174+
| <code>openkb&nbsp;skill&nbsp;eval&nbsp;&lt;name&gt;</code> | Trigger-accuracy evaluation — does the `description:` field actually fire? LLM generates eval prompts; grader LLM scores activation. `--save` persists the eval set |
175+
| <code>openkb&nbsp;skill&nbsp;history&nbsp;&lt;name&gt;</code> / <code>openkb&nbsp;skill&nbsp;rollback&nbsp;&lt;name&gt;</code> | Iteration workspace — every overwrite saves the previous version to `output/skills/<name>-workspace/iteration-N/` with a structural diff. Rollback restores any iteration |
165176

166-
`openkb chat` opens an interactive chat session over your wiki knowledge base. Unlike the one-shot `openkb query`, each turn carries the conversation history, so you can dig into a topic without re-typing context.
177+
### Query & Chat — ask the wiki
178+
179+
`openkb query "..."` answers a single question. `openkb chat` is interactive — each turn carries history, so you can dig into a topic without re-typing context. Both use the same underlying wiki and the same retrieval primitives (PageIndex for long docs, direct concept reads for short).
167180

168181
```bash
182+
openkb query "What does the literature say about attention scaling?"
183+
169184
openkb chat # start a new session
170185
openkb chat --resume # resume the most recent session
171186
openkb chat --resume 20260411 # resume by id (unique prefix works)
@@ -179,11 +194,70 @@ Inside a chat, type `/` to access slash commands (Tab to complete):
179194
- `/status` — show knowledge base status
180195
- `/list` — list all documents
181196
- `/add <path>` — add a document or directory without leaving the chat
197+
- `/skill new <name> "<intent>"` — compile a skill from this chat (see below)
182198
- `/save [name]` — export the transcript to `wiki/explorations/`
183199
- `/clear` — start a fresh session (the current one stays on disk)
184200
- `/lint` — run knowledge base lint
185201
- `/exit` — exit (Ctrl-D also works)
186202

203+
### 🛠 Skill Factory — *Drop in a book. Out comes a digital expert.*
204+
205+
The newest generator. `openkb skill new` distills any subset of your wiki into an [Anthropic Skill](https://docs.claude.com/en/docs/build-with-claude/skills) — a portable folder that **Claude Code, Codex CLI, Gemini CLI, and Cursor** all install and load natively. Drop in a book's worth of papers; out comes a specialist that other agents can call on.
206+
207+
```bash
208+
openkb skill new karpathy-thinking \
209+
"Reason about transformers and attention in Karpathy's style"
210+
```
211+
212+
This produces:
213+
214+
```
215+
<kb>/output/skills/karpathy-thinking/
216+
├── SKILL.md # YAML frontmatter + when-to-use + approach
217+
├── references/ # depth material the agent loads on demand
218+
│ ├── methodology.md
219+
│ └── key-quotes.md
220+
└── (scripts/) # optional, only if intent implies computation
221+
```
222+
223+
…plus an auto-updated `<kb>/.claude-plugin/marketplace.json` so the whole KB is one-line installable.
224+
225+
**Install locally:**
226+
227+
```bash
228+
cp -r output/skills/karpathy-thinking ~/.claude/skills/
229+
```
230+
231+
**Share with others** — push your KB to GitHub, then anyone runs:
232+
233+
```bash
234+
npx skills@latest add <your-org>/<your-repo>
235+
```
236+
237+
**Iterate from chat** — compilation is one-shot, but follow-up edits aren't. Inside `openkb chat`, you can refine without re-running the whole pipeline:
238+
239+
```
240+
/skill new karpathy-thinking "Reason about transformers like Karpathy"
241+
[generation streams]
242+
> description is too generic, make it about transformer implementations specifically
243+
[agent edits SKILL.md frontmatter in place]
244+
```
245+
246+
**Quality gates** — structural validation, trigger-accuracy + body-coverage evaluation, and full history/rollback:
247+
248+
```bash
249+
# Lint structure (auto-runs at end of `skill new`)
250+
openkb skill validate karpathy-thinking
251+
openkb skill validate --strict # treat warnings as failures
252+
253+
# Does the description actually fire when it should?
254+
openkb skill eval karpathy-thinking --save
255+
256+
# History + rollback if a new iteration regresses
257+
openkb skill history karpathy-thinking
258+
openkb skill rollback karpathy-thinking --to 2
259+
```
260+
187261
### Configuration
188262

189263
Settings are initialized by `openkb init`, and stored in `.openkb/config.yaml`:

openkb/agent/chat.py

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from prompt_toolkit.styles import Style
2424

2525
from openkb.agent.chat_session import ChatSession
26-
from openkb.agent.query import MAX_TURNS, build_query_agent
26+
from openkb.agent.query import MAX_TURNS, build_chat_agent
2727
from openkb.log import append_log
2828

2929

@@ -59,6 +59,7 @@
5959
" /list List all documents in the knowledge base\n"
6060
" /lint Lint the knowledge base\n"
6161
" /add <path> Add a document or directory to the knowledge base\n"
62+
' /skill new <name> "<intent>" Compile a skill from the wiki\n'
6263
" /help Show this"
6364
)
6465

@@ -214,6 +215,7 @@ def _bottom_toolbar(session: ChatSession) -> FormattedText:
214215
("/list", "List all documents"),
215216
("/lint", "Lint the knowledge base"),
216217
("/add", "Add a document or directory"),
218+
("/skill", "Compile a skill (try `/skill new <name> \"intent\"`)"),
217219
]
218220

219221

@@ -494,6 +496,86 @@ async def _run_add(arg: str, kb_dir: Path, style: Style) -> None:
494496
await asyncio.to_thread(add_single_file, target, kb_dir)
495497

496498

499+
async def _handle_slash_skill(arg: str, kb_dir: Path, style: Style) -> None:
500+
"""Dispatch ``/skill new <name> "<intent>"`` and any future skill subcommands."""
501+
import shlex
502+
503+
try:
504+
parts = shlex.split(arg) if arg else []
505+
except ValueError as exc:
506+
_fmt(style, ("class:error", f"[ERROR] Could not parse: {exc}\n"))
507+
return
508+
if not parts:
509+
_fmt(style, ("class:error", "Usage: /skill new <name> \"<intent>\"\n"))
510+
return
511+
512+
sub = parts[0].lower()
513+
if sub != "new":
514+
_fmt(style, ("class:error", f"Unknown skill subcommand: {sub}. Try /skill new.\n"))
515+
return
516+
517+
if len(parts) < 3:
518+
_fmt(style, ("class:error", "Usage: /skill new <name> \"<intent>\"\n"))
519+
return
520+
521+
name = parts[1]
522+
intent = " ".join(parts[2:])
523+
524+
# Use the same safety gates as the CLI (name validation, wiki dir,
525+
# wiki content). Chat doesn't have a -y flag, so existing skills
526+
# block with a clear instruction to delete first.
527+
from openkb.cli import _preflight_skill_new
528+
err = _preflight_skill_new(kb_dir, name)
529+
if err:
530+
_fmt(style, ("class:error", f"[ERROR] {err}\n"))
531+
return
532+
533+
from openkb.skill import skill_dir
534+
target = skill_dir(kb_dir, name)
535+
if target.exists():
536+
_fmt(style, ("class:error",
537+
f"[ERROR] output/skills/{name}/ already exists. Remove it first "
538+
f"with `rm -rf output/skills/{name}` and re-run.\n"))
539+
return
540+
541+
# Load model from KB config
542+
from openkb.config import load_config, DEFAULT_CONFIG
543+
config = load_config(kb_dir / ".openkb" / "config.yaml")
544+
model = config.get("model", DEFAULT_CONFIG["model"])
545+
546+
from openkb.skill.generator import Generator
547+
_fmt(style, ("class:slash.help", f"Compiling skill '{name}'...\n"))
548+
gen = Generator(
549+
target_type="skill",
550+
name=name,
551+
intent=intent,
552+
kb_dir=kb_dir,
553+
model=model,
554+
)
555+
try:
556+
await gen.run()
557+
except RuntimeError as exc:
558+
_fmt(style, ("class:error", f"[ERROR] {exc}\n"))
559+
return
560+
561+
# Surface validation issues from Generator.run (same gate as CLI).
562+
result = gen.validation
563+
if result is not None and (result.errors or result.warnings):
564+
_fmt(style, ("class:error", "[WARN] Validation found issues:\n"))
565+
for err in result.errors:
566+
_fmt(style, ("class:error", f" ERROR: {err}\n"))
567+
for warn in result.warnings:
568+
_fmt(style, ("class:error", f" WARN: {warn}\n"))
569+
_fmt(style, ("class:slash.help",
570+
f"Run `openkb skill validate {name}` to re-check, or "
571+
f"`openkb skill rollback {name}` to revert.\n"))
572+
573+
_fmt(style, ("class:slash.ok", f"Saved: output/skills/{name}/\n"))
574+
_fmt(style, ("class:slash.help",
575+
f"Iterate: ask follow-up questions in this chat and the agent can "
576+
f"edit files under output/skills/{name}/ directly.\n"))
577+
578+
497579
async def _handle_slash(
498580
cmd: str,
499581
kb_dir: Path,
@@ -557,6 +639,10 @@ async def _handle_slash(
557639
await _run_add(arg, kb_dir, style)
558640
return None
559641

642+
if head == "/skill":
643+
await _handle_slash_skill(arg, kb_dir, style)
644+
return None
645+
560646
_fmt(
561647
style,
562648
("class:error", f"Unknown command: {head}. Try /help.\n"),
@@ -579,8 +665,7 @@ async def run_chat(
579665

580666
config = load_config(kb_dir / ".openkb" / "config.yaml")
581667
language = session.language or config.get("language", "en")
582-
wiki_root = str(kb_dir / "wiki")
583-
agent = build_query_agent(wiki_root, session.model, language=language)
668+
agent = build_chat_agent(kb_dir, session.model, language=language)
584669

585670
_print_header(session, kb_dir, style)
586671
if session.turn_count > 0:
@@ -620,7 +705,7 @@ async def run_chat(
620705
return
621706
if action == "new_session":
622707
session = ChatSession.new(kb_dir, session.model, session.language)
623-
agent = build_query_agent(wiki_root, session.model, language=language)
708+
agent = build_chat_agent(kb_dir, session.model, language=language)
624709
prompt_session = _make_prompt_session(session, style, use_color, kb_dir)
625710
continue
626711

openkb/agent/query.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from agents import Agent, Runner, function_tool
77

88
from agents import ToolOutputImage, ToolOutputText
9-
from openkb.agent.tools import get_wiki_page_content, read_wiki_file, read_wiki_image
9+
from openkb.agent.tools import (
10+
get_wiki_page_content,
11+
read_wiki_file,
12+
read_wiki_image,
13+
write_kb_file,
14+
)
1015

1116
MAX_TURNS = 50
1217
from openkb.schema import get_agents_md
@@ -91,6 +96,43 @@ def get_image(image_path: str) -> ToolOutputImage | ToolOutputText:
9196
)
9297

9398

99+
def build_chat_agent(
100+
kb_dir: Path,
101+
model: str,
102+
language: str = "en",
103+
) -> Agent:
104+
"""Build the chat agent: query agent + a write tool restricted to
105+
``<kb>/wiki/explorations/**`` and ``<kb>/output/**``.
106+
107+
This is the variant used by the interactive ``openkb chat`` REPL so users
108+
can iterate on generated artifacts (e.g. ``output/skills/<name>/``) via
109+
natural-language follow-ups without giving the agent unrestricted write
110+
access to the wiki.
111+
"""
112+
wiki_root = str(kb_dir / "wiki")
113+
kb_root = str(kb_dir)
114+
base = build_query_agent(wiki_root, model, language=language)
115+
116+
@function_tool
117+
def write_file(path: str, content: str) -> str:
118+
"""Write a text file under the KB.
119+
120+
Allowed paths (relative to KB root):
121+
* ``wiki/explorations/**`` — chat-derived notes.
122+
* ``output/**`` — generator artifacts (skills, etc.).
123+
124+
Any other path is rejected. Parent directories are created.
125+
126+
Args:
127+
path: File path relative to KB root
128+
(e.g. ``"output/skills/demo/SKILL.md"``).
129+
content: Full text content to write (overwrites if file exists).
130+
"""
131+
return write_kb_file(path, content, kb_root)
132+
133+
return base.clone(tools=[*base.tools, write_file])
134+
135+
94136
async def run_query(
95137
question: str,
96138
kb_dir: Path,

0 commit comments

Comments
 (0)