knowflow 0.1.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.
- knowflow/__init__.py +4 -0
- knowflow/__main__.py +3 -0
- knowflow/adapters/knowledge/__init__.py +42 -0
- knowflow/adapters/knowledge/local.py +43 -0
- knowflow/adapters/knowledge/obsidian.py +64 -0
- knowflow/adapters/storage/__init__.py +62 -0
- knowflow/adapters/storage/git.py +155 -0
- knowflow/adapters/storage/local.py +110 -0
- knowflow/adapters/storage/quark.py +225 -0
- knowflow/adapters/storage/s3.py +118 -0
- knowflow/adapters/storage/webdav.py +196 -0
- knowflow/cli/__init__.py +265 -0
- knowflow/cli/template_manager.py +100 -0
- knowflow/core/__init__.py +10 -0
- knowflow/core/knowledge.py +85 -0
- knowflow/core/pipeline.py +119 -0
- knowflow/core/storage.py +167 -0
- knowflow/dashboard/__init__.py +355 -0
- knowflow/templates/daily-journal/plugin.yaml +21 -0
- knowflow/templates/football-prediction/plugin.yaml +22 -0
- knowflow/templates/project-wiki/plugin.yaml +20 -0
- knowflow/templates/research-notes/plugin.yaml +26 -0
- knowflow/templates/stock-analysis/plugin.yaml +22 -0
- knowflow/templates/web-monitor/plugin.yaml +20 -0
- knowflow/templates/worldcup-tracker/plugin.yaml +18 -0
- knowflow/transforms/__init__.py +6 -0
- knowflow/transforms/archive.py +87 -0
- knowflow/transforms/markdown_export.py +110 -0
- knowflow/transforms/profile_index.py +89 -0
- knowflow-0.1.0.dist-info/METADATA +264 -0
- knowflow-0.1.0.dist-info/RECORD +35 -0
- knowflow-0.1.0.dist-info/WHEEL +5 -0
- knowflow-0.1.0.dist-info/entry_points.txt +2 -0
- knowflow-0.1.0.dist-info/licenses/LICENSE +21 -0
- knowflow-0.1.0.dist-info/top_level.txt +1 -0
knowflow/cli/__init__.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
KnowFlow CLI ā Agent Knowledge Pipeline
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
knowflow init <template> [--dir DIR] [--storage git|local] [--kb obsidian|local]
|
|
7
|
+
knowflow run [--plugin NAME]
|
|
8
|
+
knowflow status
|
|
9
|
+
knowflow commit [MESSAGE]
|
|
10
|
+
knowflow archive [--dry-run]
|
|
11
|
+
knowflow list
|
|
12
|
+
knowflow version
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
args = sys.argv[1:]
|
|
20
|
+
if not args or args[0] in ("-h", "--help"):
|
|
21
|
+
print(__doc__.strip())
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
cmd = args[0]
|
|
25
|
+
|
|
26
|
+
if cmd == "version":
|
|
27
|
+
from knowflow import __version__
|
|
28
|
+
print(f"KnowFlow v{__version__}")
|
|
29
|
+
|
|
30
|
+
elif cmd == "list":
|
|
31
|
+
from knowflow.cli.template_manager import list_templates
|
|
32
|
+
templates = list_templates()
|
|
33
|
+
print(f"\nš¦ Available templates ({len(templates)}):\n")
|
|
34
|
+
for t in templates:
|
|
35
|
+
print(f" {t['display']:25s} {t['description']}")
|
|
36
|
+
print()
|
|
37
|
+
|
|
38
|
+
elif cmd == "init":
|
|
39
|
+
_init(args[1:])
|
|
40
|
+
|
|
41
|
+
elif cmd == "run":
|
|
42
|
+
_run(args[1:])
|
|
43
|
+
|
|
44
|
+
elif cmd == "status":
|
|
45
|
+
_status()
|
|
46
|
+
|
|
47
|
+
elif cmd == "commit":
|
|
48
|
+
msg = " ".join(args[1:]) if len(args) > 1 else ""
|
|
49
|
+
_commit(msg)
|
|
50
|
+
|
|
51
|
+
elif cmd == "archive":
|
|
52
|
+
dry_run = "--dry-run" in args
|
|
53
|
+
_archive(dry_run)
|
|
54
|
+
|
|
55
|
+
elif cmd == "dashboard":
|
|
56
|
+
_dashboard(args[1:])
|
|
57
|
+
|
|
58
|
+
else:
|
|
59
|
+
print(f"Unknown command: {cmd}")
|
|
60
|
+
print(__doc__.strip())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _init(args):
|
|
64
|
+
from knowflow.cli.template_manager import init_project, list_templates
|
|
65
|
+
|
|
66
|
+
template = args[0] if args else None
|
|
67
|
+
output_dir = "."
|
|
68
|
+
storage = "git"
|
|
69
|
+
kb = "local"
|
|
70
|
+
|
|
71
|
+
for i, a in enumerate(args):
|
|
72
|
+
if a == "--dir" and i + 1 < len(args): output_dir = args[i + 1]
|
|
73
|
+
if a == "--storage" and i + 1 < len(args): storage = args[i + 1]
|
|
74
|
+
if a == "--kb" and i + 1 < len(args): kb = args[i + 1]
|
|
75
|
+
|
|
76
|
+
if not template:
|
|
77
|
+
print("ā Please specify a template. Available:")
|
|
78
|
+
for t in list_templates():
|
|
79
|
+
print(f" {t['name']:25s} {t['description']}")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
files = init_project(template, output_dir, storage, kb)
|
|
84
|
+
print(f"\nā
Created project '{template}' in {output_dir}/")
|
|
85
|
+
for f in files:
|
|
86
|
+
print(f" š {f}")
|
|
87
|
+
print(f"\n Next: cd {output_dir} && knowflow run")
|
|
88
|
+
except ValueError as e:
|
|
89
|
+
print(f"ā {e}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run(args):
|
|
93
|
+
from knowflow.core.pipeline import PipelineRunner
|
|
94
|
+
from knowflow.transforms import MarkdownExport, ProfileIndex
|
|
95
|
+
from knowflow.adapters.storage import create_storage
|
|
96
|
+
|
|
97
|
+
# Detect project root
|
|
98
|
+
root = Path.cwd()
|
|
99
|
+
config_path = root / "knowflow.toml"
|
|
100
|
+
if not config_path.exists():
|
|
101
|
+
print("ā No knowflow.toml found. Run 'knowflow init' first.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
import yaml
|
|
105
|
+
with open(config_path) as f:
|
|
106
|
+
config = yaml.safe_load(f)
|
|
107
|
+
|
|
108
|
+
# Parse args
|
|
109
|
+
plugin_name = None
|
|
110
|
+
dry_run = "--dry-run" in args
|
|
111
|
+
verbose = "--verbose" in args
|
|
112
|
+
for i, a in enumerate(args):
|
|
113
|
+
if a == "--plugin" and i + 1 < len(args):
|
|
114
|
+
plugin_name = args[i + 1]
|
|
115
|
+
|
|
116
|
+
# Create storage
|
|
117
|
+
storage_cfg = config.get("storage", {"backend": "git", "path": str(root)})
|
|
118
|
+
store = create_storage(storage_cfg["backend"], config=storage_cfg)
|
|
119
|
+
|
|
120
|
+
# Setup pipeline
|
|
121
|
+
runner = PipelineRunner(config)
|
|
122
|
+
runner.register(MarkdownExport())
|
|
123
|
+
runner.register(ProfileIndex())
|
|
124
|
+
|
|
125
|
+
data_dir = root / "data" if (root / "data").exists() else root
|
|
126
|
+
output_dir = root / "output"
|
|
127
|
+
output_dir.mkdir(exist_ok=True)
|
|
128
|
+
|
|
129
|
+
if plugin_name:
|
|
130
|
+
results = [runner.run_plugin(plugin_name, store, data_dir, output_dir, dry_run, verbose)]
|
|
131
|
+
else:
|
|
132
|
+
plugins = [p.stem for p in sorted(data_dir.iterdir()) if p.is_dir() and p.name not in ("output", ".git", ".github")]
|
|
133
|
+
if not plugins:
|
|
134
|
+
# Fall back to the data directory itself as a single plugin
|
|
135
|
+
plugins = [root.name]
|
|
136
|
+
results = runner.run_all(plugins, store, data_dir, output_dir, dry_run, verbose)
|
|
137
|
+
|
|
138
|
+
for r in results:
|
|
139
|
+
icon = "ā
" if r["success"] else "ā"
|
|
140
|
+
print(f" {icon} {r['plugin']}")
|
|
141
|
+
for sname, sres in r["stages"].items():
|
|
142
|
+
status = sres.get("status", "?")
|
|
143
|
+
if status == "ok":
|
|
144
|
+
gen = sres.get("generated", 0)
|
|
145
|
+
print(f" āā {sname}: {gen} files generated" if gen else f" āā {sname}: ā
")
|
|
146
|
+
elif status == "skipped":
|
|
147
|
+
print(f" āā {sname}: āļø {sres.get('reason', '')}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _status():
|
|
151
|
+
from knowflow.adapters.storage import create_storage
|
|
152
|
+
|
|
153
|
+
root = Path.cwd()
|
|
154
|
+
config_path = root / "knowflow.toml"
|
|
155
|
+
backend = "git"
|
|
156
|
+
if config_path.exists():
|
|
157
|
+
import yaml
|
|
158
|
+
with open(config_path) as f:
|
|
159
|
+
cfg = yaml.safe_load(f)
|
|
160
|
+
backend = cfg.get("storage", {}).get("backend", "git")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
store = create_storage(backend, config={"path": str(root)})
|
|
164
|
+
items = store.walk() if hasattr(store, 'walk') else []
|
|
165
|
+
commits = store.log(5) if hasattr(store, 'log') else []
|
|
166
|
+
|
|
167
|
+
print(f"\nš KnowFlow Status")
|
|
168
|
+
print(f" Root: {root}")
|
|
169
|
+
print(f" Backend: {backend}")
|
|
170
|
+
print(f" Files: {len(items)}")
|
|
171
|
+
if commits:
|
|
172
|
+
print(f" Last commits:")
|
|
173
|
+
for c in commits:
|
|
174
|
+
short = c['id'][:8]
|
|
175
|
+
msg = c['message'][:50]
|
|
176
|
+
print(f" {short} {msg}")
|
|
177
|
+
else:
|
|
178
|
+
print(f" (no version history)")
|
|
179
|
+
print()
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"ā Status error: {e}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _commit(message: str = ""):
|
|
185
|
+
from knowflow.adapters.storage import create_storage
|
|
186
|
+
root = Path.cwd()
|
|
187
|
+
store = create_storage("git", config={"path": str(root)})
|
|
188
|
+
rev = store.commit(message)
|
|
189
|
+
if rev:
|
|
190
|
+
print(f"ā
Committed: {rev[:8]} ā {message or 'auto'}")
|
|
191
|
+
else:
|
|
192
|
+
print(" (nothing to commit)")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _archive(dry_run: bool = False):
|
|
196
|
+
from knowflow.transforms.archive import ArchiveStage
|
|
197
|
+
from knowflow.core.pipeline import PipelineContext
|
|
198
|
+
|
|
199
|
+
root = Path.cwd()
|
|
200
|
+
data_dir = root / "data"
|
|
201
|
+
if not data_dir.exists():
|
|
202
|
+
print("ā No data/ directory found")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
stage = ArchiveStage()
|
|
206
|
+
for plugin_dir in sorted(data_dir.iterdir()):
|
|
207
|
+
if plugin_dir.is_dir():
|
|
208
|
+
ctx = PipelineContext(
|
|
209
|
+
storage=None, plugin_name=plugin_dir.name,
|
|
210
|
+
plugin_config={}, data_dir=plugin_dir,
|
|
211
|
+
output_dir=root / "output" / plugin_dir.name,
|
|
212
|
+
dry_run=dry_run,
|
|
213
|
+
)
|
|
214
|
+
result = stage.process(ctx)
|
|
215
|
+
if dry_run:
|
|
216
|
+
print(f" {plugin_dir}: {result.get('candidates', 0)} files to archive")
|
|
217
|
+
else:
|
|
218
|
+
print(f" {plugin_dir}: {result.get('archives', 0)} archives, {result.get('freed_kb', 0)}KB freed")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _dashboard(args):
|
|
222
|
+
"""Generate web dashboard from knowledge data."""
|
|
223
|
+
from knowflow.dashboard import generate
|
|
224
|
+
|
|
225
|
+
root = Path.cwd()
|
|
226
|
+
knowledge_dir = root
|
|
227
|
+
auto_detect = True
|
|
228
|
+
password = None
|
|
229
|
+
|
|
230
|
+
for i, a in enumerate(args):
|
|
231
|
+
if a == "--dir" and i + 1 < len(args):
|
|
232
|
+
knowledge_dir = Path(args[i + 1])
|
|
233
|
+
auto_detect = False
|
|
234
|
+
if a == "--password" and i + 1 < len(args):
|
|
235
|
+
password = args[i + 1]
|
|
236
|
+
|
|
237
|
+
if auto_detect and not (knowledge_dir / "MANIFEST.yaml").exists():
|
|
238
|
+
home_kb = Path.home() / "knowledge"
|
|
239
|
+
if home_kb.exists():
|
|
240
|
+
knowledge_dir = home_kb
|
|
241
|
+
else:
|
|
242
|
+
print("ā No knowledge directory found. Specify: knowflow dashboard --dir /path/to/knowledge")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
output = knowledge_dir / "dashboard"
|
|
246
|
+
for i, a in enumerate(args):
|
|
247
|
+
if a == "--output" and i + 1 < len(args):
|
|
248
|
+
output = Path(args[i + 1])
|
|
249
|
+
|
|
250
|
+
if not password:
|
|
251
|
+
import getpass
|
|
252
|
+
password = getpass.getpass("设置仪蔨ēåÆē : ")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
path = generate(knowledge_dir, output, password=password)
|
|
256
|
+
print(f" Open: file://{path.absolute()}")
|
|
257
|
+
print(f" Host with: cd {output} && python3 -m http.server 8080")
|
|
258
|
+
except Exception as e:
|
|
259
|
+
print(f"ā Dashboard generation failed: {e}")
|
|
260
|
+
import traceback
|
|
261
|
+
traceback.print_exc()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
main()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
knowflow/cli/template_manager.py ā Template system
|
|
3
|
+
====================================================
|
|
4
|
+
Init new projects from built-in templates.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
knowflow init football # Create football prediction project
|
|
8
|
+
knowflow list # List available templates
|
|
9
|
+
"""
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import shutil
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_templates() -> list[dict]:
|
|
18
|
+
"""List all available templates with descriptions"""
|
|
19
|
+
templates = []
|
|
20
|
+
for tdir in sorted(TEMPLATES_DIR.iterdir()):
|
|
21
|
+
if tdir.is_dir():
|
|
22
|
+
cfg_path = tdir / "plugin.yaml"
|
|
23
|
+
if cfg_path.exists():
|
|
24
|
+
with open(cfg_path) as f:
|
|
25
|
+
cfg = yaml.safe_load(f) or {}
|
|
26
|
+
templates.append({
|
|
27
|
+
"name": tdir.name,
|
|
28
|
+
"display": cfg.get("display_name", tdir.name),
|
|
29
|
+
"description": cfg.get("description", ""),
|
|
30
|
+
})
|
|
31
|
+
return templates
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def init_project(template_name: str, output_dir: str, storage_backend: str = "git",
|
|
35
|
+
kb_backend: str = "local") -> list[str]:
|
|
36
|
+
"""
|
|
37
|
+
Initialize a new project from a template.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
template_name: Name of template (e.g. "football-prediction")
|
|
41
|
+
output_dir: Where to create the project
|
|
42
|
+
storage_backend: Storage type (git, local, s3, etc.)
|
|
43
|
+
kb_backend: Knowledge base type (obsidian, local, notion, etc.)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of created files (relative to output_dir)
|
|
47
|
+
"""
|
|
48
|
+
src = TEMPLATES_DIR / template_name
|
|
49
|
+
if not src.exists():
|
|
50
|
+
raise ValueError(f"Template '{template_name}' not found. "
|
|
51
|
+
f"Available: {[t['name'] for t in list_templates()]}")
|
|
52
|
+
|
|
53
|
+
dst = Path(output_dir).expanduser().resolve()
|
|
54
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
created = []
|
|
57
|
+
|
|
58
|
+
# Copy plugin config
|
|
59
|
+
if (src / "plugin.yaml").exists():
|
|
60
|
+
shutil.copy2(src / "plugin.yaml", dst / "plugin.yaml")
|
|
61
|
+
created.append("plugin.yaml")
|
|
62
|
+
|
|
63
|
+
# Create data directory
|
|
64
|
+
plugin_name = template_name.split("-")[0]
|
|
65
|
+
(dst / plugin_name).mkdir(parents=True, exist_ok=True)
|
|
66
|
+
created.append(f"{plugin_name}/")
|
|
67
|
+
|
|
68
|
+
# Create knowflow.toml config
|
|
69
|
+
config = {
|
|
70
|
+
"project": {"name": template_name, "version": "0.1.0"},
|
|
71
|
+
"storage": {"backend": storage_backend, "path": str(dst)},
|
|
72
|
+
"knowledge": {"backend": kb_backend, "path": str(dst / "output")},
|
|
73
|
+
"pipeline": ["markdown_export", "profile_index"],
|
|
74
|
+
}
|
|
75
|
+
config_path = dst / "knowflow.toml"
|
|
76
|
+
config_path.write_text(yaml.dump(config, default_flow_style=False), encoding="utf-8")
|
|
77
|
+
created.append("knowflow.toml")
|
|
78
|
+
|
|
79
|
+
# Create README
|
|
80
|
+
readme = f"""# {template_name}
|
|
81
|
+
|
|
82
|
+
Generated from KnowFlow template: {template_name}
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Add data
|
|
88
|
+
echo '{{"key": "value"}}' > {plugin_name}/data/my-data.json
|
|
89
|
+
|
|
90
|
+
# Run pipeline
|
|
91
|
+
knowflow run
|
|
92
|
+
|
|
93
|
+
# View output
|
|
94
|
+
ls {plugin_name}/output/
|
|
95
|
+
```
|
|
96
|
+
"""
|
|
97
|
+
(dst / "README.md").write_text(readme, encoding="utf-8")
|
|
98
|
+
created.append("README.md")
|
|
99
|
+
|
|
100
|
+
return created
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""knowflow/core/__init__.py"""
|
|
2
|
+
from .storage import StorageAdapter, StorageItem, StorageError, StorageNotFoundError
|
|
3
|
+
from .knowledge import KnowledgeAdapter, KnowledgePage, KnowledgeError
|
|
4
|
+
from .pipeline import TransformStage, PipelineRunner, PipelineContext, PipelineError
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'StorageAdapter', 'StorageItem', 'StorageError', 'StorageNotFoundError',
|
|
8
|
+
'KnowledgeAdapter', 'KnowledgePage', 'KnowledgeError',
|
|
9
|
+
'TransformStage', 'PipelineRunner', 'PipelineContext', 'PipelineError',
|
|
10
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
knowflow/core/knowledge.py ā Knowledge Base Adapter Interface
|
|
3
|
+
==============================================================
|
|
4
|
+
Knowledge Base = HOW humans read and interact with the data.
|
|
5
|
+
Different from Storage (where data lives).
|
|
6
|
+
|
|
7
|
+
Built-in adapters:
|
|
8
|
+
- local: Plain markdown directory
|
|
9
|
+
- obsidian: Obsidian vault (markdown + .obsidian config)
|
|
10
|
+
- notion: Notion database (via API)
|
|
11
|
+
- ima: č
¾č®ÆIMAē„čÆåŗ (via OpenAPI)
|
|
12
|
+
- mkdocs: MkDocs static site
|
|
13
|
+
- gitbook: GitBook-style documentation
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from knowflow.adapters.knowledge import create_knowledge, create_kb # create_kb äøŗå«å
|
|
17
|
+
kb = create_knowledge("obsidian", path="~/my-vault")
|
|
18
|
+
kb.sync([KnowledgePage(title="åØę„", content="# åØę„\\nā¦", path="weekly/2026-10-02.md")])
|
|
19
|
+
kb.search("é
čÆ»ä¹ ęÆ", limit=5)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KnowledgeError(Exception): pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class KnowledgePage:
|
|
32
|
+
"""A page/document in the knowledge base"""
|
|
33
|
+
title: str
|
|
34
|
+
content: str # Markdown content
|
|
35
|
+
path: str # Relative path within KB
|
|
36
|
+
tags: list[str] = None
|
|
37
|
+
metadata: dict = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class KnowledgeAdapter(ABC):
|
|
41
|
+
"""
|
|
42
|
+
Knowledge Base Adapter Interface
|
|
43
|
+
|
|
44
|
+
Transforms structured data from Storage into human-readable
|
|
45
|
+
documents in the target knowledge base system.
|
|
46
|
+
|
|
47
|
+
A single data source can sync to multiple KBs simultaneously
|
|
48
|
+
(e.g., Push to Obsidian + Notion + IMA at the same time).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, config: dict = None):
|
|
52
|
+
self.config = config or {}
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def name(self) -> str:
|
|
56
|
+
"""Human-readable name of this KB backend"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def sync(self, pages: list[KnowledgePage]) -> dict:
|
|
61
|
+
"""
|
|
62
|
+
Sync pages to the knowledge base.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
pages: List of KnowledgePage objects to sync
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
{"synced": 5, "skipped": 2, "errors": [...]}
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def search(self, query: str, limit: int = 10) -> list[KnowledgePage]:
|
|
74
|
+
"""
|
|
75
|
+
Search the knowledge base.
|
|
76
|
+
Returns matching pages.
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def validate_config(self) -> list[str]:
|
|
81
|
+
"""
|
|
82
|
+
Check if configuration is valid.
|
|
83
|
+
Returns list of error messages (empty = valid).
|
|
84
|
+
"""
|
|
85
|
+
return []
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
knowflow/core/pipeline.py ā Data Pipeline Framework
|
|
3
|
+
====================================================
|
|
4
|
+
Transforms raw data (JSON) ā formatted output (MD/docs).
|
|
5
|
+
Each transform is a pluggable stage.
|
|
6
|
+
|
|
7
|
+
Built-in stages:
|
|
8
|
+
- markdown_export: JSON ā Markdown
|
|
9
|
+
- profile_index: Auto-generate PROFILE.md
|
|
10
|
+
- archive: Hot ā Warm ā Cold lifecycle
|
|
11
|
+
- token_compress: TokenJuice-style compression
|
|
12
|
+
- validate_schema: Validate against plugin schema
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import importlib
|
|
19
|
+
import inspect
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PipelineContext:
|
|
25
|
+
"""Context passed through each pipeline stage"""
|
|
26
|
+
storage: object # StorageAdapter instance
|
|
27
|
+
plugin_name: str # e.g. "football"
|
|
28
|
+
plugin_config: dict # Parsed plugin.yaml
|
|
29
|
+
data_dir: Path # Where data files live
|
|
30
|
+
output_dir: Path # Where output goes (obsidian/)
|
|
31
|
+
dry_run: bool = False
|
|
32
|
+
verbose: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PipelineError(Exception): pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TransformStage(ABC):
|
|
39
|
+
"""
|
|
40
|
+
A single stage in the data pipeline.
|
|
41
|
+
|
|
42
|
+
Stages are loaded from knowflow.toml [pipeline] config.
|
|
43
|
+
To add a custom stage: subclass TransformStage and register.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str):
|
|
47
|
+
self.name = name
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def process(self, ctx: PipelineContext) -> dict:
|
|
51
|
+
"""
|
|
52
|
+
Execute this stage.
|
|
53
|
+
Returns: {"status": "ok"|"skipped"|"error", "detail": "..."}
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def should_run(self, ctx: PipelineContext) -> bool:
|
|
58
|
+
"""Override to add conditional execution."""
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PipelineRunner:
|
|
63
|
+
"""Orchestrates pipeline execution"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, config: dict):
|
|
66
|
+
self.config = config
|
|
67
|
+
self.stages: dict[str, TransformStage] = {}
|
|
68
|
+
|
|
69
|
+
def register(self, stage: TransformStage):
|
|
70
|
+
"""Register a pipeline stage"""
|
|
71
|
+
self.stages[stage.name] = stage
|
|
72
|
+
|
|
73
|
+
def load_plugin_config(self, plugin_dir: Path) -> dict:
|
|
74
|
+
"""Load plugin.yaml from a plugin directory"""
|
|
75
|
+
path = plugin_dir / "plugin.yaml"
|
|
76
|
+
if path.exists():
|
|
77
|
+
with open(path) as f:
|
|
78
|
+
return yaml.safe_load(f) or {}
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
def run_plugin(self, plugin_name: str, storage, data_dir: Path, output_dir: Path,
|
|
82
|
+
dry_run: bool = False, verbose: bool = False) -> dict:
|
|
83
|
+
"""Run pipeline for one plugin"""
|
|
84
|
+
result = {"plugin": plugin_name, "stages": {}, "success": True}
|
|
85
|
+
|
|
86
|
+
ctx = PipelineContext(
|
|
87
|
+
storage=storage,
|
|
88
|
+
plugin_name=plugin_name,
|
|
89
|
+
plugin_config={},
|
|
90
|
+
data_dir=data_dir / plugin_name,
|
|
91
|
+
output_dir=output_dir / plugin_name,
|
|
92
|
+
dry_run=dry_run,
|
|
93
|
+
verbose=verbose,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
for stage_name, stage in self.stages.items():
|
|
97
|
+
if not stage.should_run(ctx):
|
|
98
|
+
result["stages"][stage_name] = {"status": "skipped"}
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
stage_result = stage.process(ctx)
|
|
103
|
+
result["stages"][stage_name] = stage_result
|
|
104
|
+
if stage_result.get("status") == "error":
|
|
105
|
+
result["success"] = False
|
|
106
|
+
except Exception as e:
|
|
107
|
+
result["stages"][stage_name] = {"status": "error", "detail": str(e)}
|
|
108
|
+
result["success"] = False
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def run_all(self, plugins: list, storage, data_dir: Path, output_dir: Path,
|
|
113
|
+
dry_run: bool = False, verbose: bool = False) -> list[dict]:
|
|
114
|
+
"""Run pipeline for all plugins"""
|
|
115
|
+
results = []
|
|
116
|
+
for plugin in plugins:
|
|
117
|
+
result = self.run_plugin(plugin, storage, data_dir, output_dir, dry_run, verbose)
|
|
118
|
+
results.append(result)
|
|
119
|
+
return results
|