@geravant/sinain 1.0.1
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.
- package/README.md +183 -0
- package/index.ts +2096 -0
- package/install.js +155 -0
- package/openclaw.plugin.json +59 -0
- package/package.json +21 -0
- package/sinain-memory/common.py +403 -0
- package/sinain-memory/demo_knowledge_transfer.sh +85 -0
- package/sinain-memory/embedder.py +268 -0
- package/sinain-memory/eval/__init__.py +0 -0
- package/sinain-memory/eval/assertions.py +288 -0
- package/sinain-memory/eval/judges/__init__.py +0 -0
- package/sinain-memory/eval/judges/base_judge.py +61 -0
- package/sinain-memory/eval/judges/curation_judge.py +46 -0
- package/sinain-memory/eval/judges/insight_judge.py +48 -0
- package/sinain-memory/eval/judges/mining_judge.py +42 -0
- package/sinain-memory/eval/judges/signal_judge.py +45 -0
- package/sinain-memory/eval/schemas.py +247 -0
- package/sinain-memory/eval_delta.py +109 -0
- package/sinain-memory/eval_reporter.py +642 -0
- package/sinain-memory/feedback_analyzer.py +221 -0
- package/sinain-memory/git_backup.sh +19 -0
- package/sinain-memory/insight_synthesizer.py +181 -0
- package/sinain-memory/memory/2026-03-01.md +11 -0
- package/sinain-memory/memory/playbook-archive/sinain-playbook-2026-03-01-1418.md +15 -0
- package/sinain-memory/memory/playbook-logs/2026-03-01.jsonl +1 -0
- package/sinain-memory/memory/sinain-playbook.md +21 -0
- package/sinain-memory/memory-config.json +39 -0
- package/sinain-memory/memory_miner.py +183 -0
- package/sinain-memory/module_manager.py +695 -0
- package/sinain-memory/playbook_curator.py +225 -0
- package/sinain-memory/requirements.txt +3 -0
- package/sinain-memory/signal_analyzer.py +141 -0
- package/sinain-memory/test_local.py +402 -0
- package/sinain-memory/tests/__init__.py +0 -0
- package/sinain-memory/tests/conftest.py +189 -0
- package/sinain-memory/tests/test_curator_helpers.py +94 -0
- package/sinain-memory/tests/test_embedder.py +210 -0
- package/sinain-memory/tests/test_extract_json.py +124 -0
- package/sinain-memory/tests/test_feedback_computation.py +121 -0
- package/sinain-memory/tests/test_miner_helpers.py +71 -0
- package/sinain-memory/tests/test_module_management.py +458 -0
- package/sinain-memory/tests/test_parsers.py +96 -0
- package/sinain-memory/tests/test_tick_evaluator.py +430 -0
- package/sinain-memory/tests/test_triple_extractor.py +255 -0
- package/sinain-memory/tests/test_triple_ingest.py +191 -0
- package/sinain-memory/tests/test_triple_migrate.py +138 -0
- package/sinain-memory/tests/test_triplestore.py +248 -0
- package/sinain-memory/tick_evaluator.py +392 -0
- package/sinain-memory/triple_extractor.py +402 -0
- package/sinain-memory/triple_ingest.py +290 -0
- package/sinain-memory/triple_migrate.py +275 -0
- package/sinain-memory/triple_query.py +184 -0
- package/sinain-memory/triplestore.py +498 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Module Manager — CLI for managing sinain knowledge modules.
|
|
3
|
+
|
|
4
|
+
Management subcommands (no LLM):
|
|
5
|
+
list, activate, suspend, priority, stack, info, guidance
|
|
6
|
+
|
|
7
|
+
Extraction subcommand (uses LLM):
|
|
8
|
+
extract — reads playbook + logs, uses LLM to extract domain patterns
|
|
9
|
+
|
|
10
|
+
Transfer subcommands (no LLM):
|
|
11
|
+
export — package a module as a portable .sinain-module.json bundle
|
|
12
|
+
import — import a module from a .sinain-module.json bundle
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 module_manager.py --modules-dir modules/ list
|
|
16
|
+
python3 module_manager.py --modules-dir modules/ activate react-native-dev --priority 85
|
|
17
|
+
python3 module_manager.py --modules-dir modules/ suspend react-native-dev
|
|
18
|
+
python3 module_manager.py --modules-dir modules/ priority react-native-dev 90
|
|
19
|
+
python3 module_manager.py --modules-dir modules/ stack
|
|
20
|
+
python3 module_manager.py --modules-dir modules/ info react-native-dev
|
|
21
|
+
python3 module_manager.py --modules-dir modules/ guidance react-native-dev
|
|
22
|
+
python3 module_manager.py --modules-dir modules/ guidance react-native-dev \\
|
|
23
|
+
--set "When user asks about hot reload, suggest Hermes bytecode caching"
|
|
24
|
+
python3 module_manager.py --modules-dir modules/ guidance react-native-dev --clear
|
|
25
|
+
python3 module_manager.py --modules-dir modules/ extract new-domain \\
|
|
26
|
+
--domain "description" --memory-dir memory/ [--min-score 0.3]
|
|
27
|
+
python3 module_manager.py --modules-dir modules/ export ocr-pipeline \\
|
|
28
|
+
[--output /tmp/ocr.sinain-module.json]
|
|
29
|
+
python3 module_manager.py --modules-dir modules/ import bundle.sinain-module.json \\
|
|
30
|
+
[--activate] [--force]
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import sys
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Registry helpers
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def _registry_path(modules_dir: Path) -> Path:
|
|
45
|
+
return modules_dir / "module-registry.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _load_registry(modules_dir: Path) -> dict:
|
|
49
|
+
path = _registry_path(modules_dir)
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return {"version": 1, "modules": {}}
|
|
52
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _save_registry(modules_dir: Path, registry: dict) -> None:
|
|
56
|
+
path = _registry_path(modules_dir)
|
|
57
|
+
path.write_text(json.dumps(registry, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_manifest(modules_dir: Path, module_id: str) -> dict | None:
|
|
61
|
+
manifest_path = modules_dir / module_id / "manifest.json"
|
|
62
|
+
if not manifest_path.exists():
|
|
63
|
+
return None
|
|
64
|
+
return json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _now_iso() -> str:
|
|
68
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _error(msg: str) -> None:
|
|
72
|
+
"""Print error as JSON and exit."""
|
|
73
|
+
print(json.dumps({"error": msg}, ensure_ascii=False))
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Subcommands: management (no LLM)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def cmd_list(modules_dir: Path, _args: argparse.Namespace) -> None:
|
|
82
|
+
"""List all registered modules with their status."""
|
|
83
|
+
registry = _load_registry(modules_dir)
|
|
84
|
+
modules = []
|
|
85
|
+
for mid, entry in registry.get("modules", {}).items():
|
|
86
|
+
manifest = _load_manifest(modules_dir, mid)
|
|
87
|
+
modules.append({
|
|
88
|
+
"id": mid,
|
|
89
|
+
"name": manifest.get("name", mid) if manifest else mid,
|
|
90
|
+
"status": entry.get("status", "unknown"),
|
|
91
|
+
"priority": entry.get("priority", 0),
|
|
92
|
+
"locked": entry.get("locked", False),
|
|
93
|
+
"hasPatterns": (modules_dir / mid / "patterns.md").exists(),
|
|
94
|
+
})
|
|
95
|
+
# Also list unregistered module dirs
|
|
96
|
+
for child in sorted(modules_dir.iterdir()):
|
|
97
|
+
if child.is_dir() and child.name not in registry.get("modules", {}):
|
|
98
|
+
manifest = _load_manifest(modules_dir, child.name)
|
|
99
|
+
if manifest:
|
|
100
|
+
modules.append({
|
|
101
|
+
"id": child.name,
|
|
102
|
+
"name": manifest.get("name", child.name),
|
|
103
|
+
"status": "unregistered",
|
|
104
|
+
"priority": manifest.get("priority", {}).get("default", 0) if isinstance(manifest.get("priority"), dict) else 0,
|
|
105
|
+
"locked": False,
|
|
106
|
+
"hasPatterns": (child / "patterns.md").exists(),
|
|
107
|
+
})
|
|
108
|
+
print(json.dumps({"modules": modules}, ensure_ascii=False))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_activate(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
112
|
+
"""Activate a module (optionally set priority)."""
|
|
113
|
+
module_id = args.module_id
|
|
114
|
+
manifest = _load_manifest(modules_dir, module_id)
|
|
115
|
+
if not manifest:
|
|
116
|
+
_error(f"Module '{module_id}' not found (no manifest.json in {modules_dir / module_id})")
|
|
117
|
+
|
|
118
|
+
registry = _load_registry(modules_dir)
|
|
119
|
+
entry = registry.get("modules", {}).get(module_id, {})
|
|
120
|
+
|
|
121
|
+
# Determine priority
|
|
122
|
+
priority = args.priority
|
|
123
|
+
if priority is None:
|
|
124
|
+
priority = entry.get("priority") or (
|
|
125
|
+
manifest.get("priority", {}).get("default", 70)
|
|
126
|
+
if isinstance(manifest.get("priority"), dict)
|
|
127
|
+
else 70
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Validate priority against manifest range
|
|
131
|
+
prio_range = manifest.get("priority", {}).get("range") if isinstance(manifest.get("priority"), dict) else None
|
|
132
|
+
if prio_range and len(prio_range) == 2:
|
|
133
|
+
lo, hi = prio_range
|
|
134
|
+
if not (lo <= priority <= hi):
|
|
135
|
+
_error(f"Priority {priority} outside allowed range [{lo}, {hi}] for module '{module_id}'")
|
|
136
|
+
|
|
137
|
+
# Update registry
|
|
138
|
+
registry.setdefault("modules", {})[module_id] = {
|
|
139
|
+
"status": "active",
|
|
140
|
+
"priority": priority,
|
|
141
|
+
"activatedAt": _now_iso(),
|
|
142
|
+
"lastTriggered": entry.get("lastTriggered"),
|
|
143
|
+
"locked": entry.get("locked", manifest.get("locked", False)),
|
|
144
|
+
}
|
|
145
|
+
_save_registry(modules_dir, registry)
|
|
146
|
+
|
|
147
|
+
# Fire-and-forget: ingest module patterns into triple store
|
|
148
|
+
import subprocess
|
|
149
|
+
try:
|
|
150
|
+
subprocess.run(
|
|
151
|
+
["python3", "triple_ingest.py", "--memory-dir",
|
|
152
|
+
str(modules_dir.parent / "memory"),
|
|
153
|
+
"--ingest-module", module_id, "--modules-dir", str(modules_dir),
|
|
154
|
+
"--embed"],
|
|
155
|
+
capture_output=True, timeout=15,
|
|
156
|
+
cwd=str(Path(__file__).parent),
|
|
157
|
+
)
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
print(json.dumps({
|
|
162
|
+
"activated": module_id,
|
|
163
|
+
"priority": priority,
|
|
164
|
+
"status": "active",
|
|
165
|
+
}, ensure_ascii=False))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_suspend(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
169
|
+
"""Suspend a module (patterns excluded from effective playbook)."""
|
|
170
|
+
module_id = args.module_id
|
|
171
|
+
registry = _load_registry(modules_dir)
|
|
172
|
+
entry = registry.get("modules", {}).get(module_id)
|
|
173
|
+
|
|
174
|
+
if not entry:
|
|
175
|
+
_error(f"Module '{module_id}' not found in registry")
|
|
176
|
+
if entry.get("locked"):
|
|
177
|
+
_error(f"Module '{module_id}' is locked and cannot be suspended")
|
|
178
|
+
|
|
179
|
+
entry["status"] = "suspended"
|
|
180
|
+
_save_registry(modules_dir, registry)
|
|
181
|
+
|
|
182
|
+
# Fire-and-forget: retract module patterns from triple store
|
|
183
|
+
import subprocess
|
|
184
|
+
try:
|
|
185
|
+
subprocess.run(
|
|
186
|
+
["python3", "triple_ingest.py", "--memory-dir",
|
|
187
|
+
str(modules_dir.parent / "memory"),
|
|
188
|
+
"--retract-module", module_id],
|
|
189
|
+
capture_output=True, timeout=15,
|
|
190
|
+
cwd=str(Path(__file__).parent),
|
|
191
|
+
)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
print(json.dumps({"suspended": module_id}, ensure_ascii=False))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_priority(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
199
|
+
"""Change a module's priority."""
|
|
200
|
+
module_id = args.module_id
|
|
201
|
+
new_priority = args.new_priority
|
|
202
|
+
|
|
203
|
+
manifest = _load_manifest(modules_dir, module_id)
|
|
204
|
+
if not manifest:
|
|
205
|
+
_error(f"Module '{module_id}' not found")
|
|
206
|
+
|
|
207
|
+
# Validate against manifest range
|
|
208
|
+
prio_range = manifest.get("priority", {}).get("range") if isinstance(manifest.get("priority"), dict) else None
|
|
209
|
+
if prio_range and len(prio_range) == 2:
|
|
210
|
+
lo, hi = prio_range
|
|
211
|
+
if not (lo <= new_priority <= hi):
|
|
212
|
+
_error(f"Priority {new_priority} outside allowed range [{lo}, {hi}]")
|
|
213
|
+
|
|
214
|
+
registry = _load_registry(modules_dir)
|
|
215
|
+
entry = registry.get("modules", {}).get(module_id)
|
|
216
|
+
if not entry:
|
|
217
|
+
_error(f"Module '{module_id}' not in registry (activate it first)")
|
|
218
|
+
|
|
219
|
+
entry["priority"] = new_priority
|
|
220
|
+
_save_registry(modules_dir, registry)
|
|
221
|
+
print(json.dumps({
|
|
222
|
+
"module": module_id,
|
|
223
|
+
"priority": new_priority,
|
|
224
|
+
}, ensure_ascii=False))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cmd_stack(modules_dir: Path, _args: argparse.Namespace) -> None:
|
|
228
|
+
"""Show the active module stack (sorted by priority desc)."""
|
|
229
|
+
registry = _load_registry(modules_dir)
|
|
230
|
+
active = []
|
|
231
|
+
suspended = []
|
|
232
|
+
for mid, entry in registry.get("modules", {}).items():
|
|
233
|
+
info = {
|
|
234
|
+
"id": mid,
|
|
235
|
+
"priority": entry.get("priority", 0),
|
|
236
|
+
"locked": entry.get("locked", False),
|
|
237
|
+
}
|
|
238
|
+
if entry.get("status") == "active":
|
|
239
|
+
active.append(info)
|
|
240
|
+
elif entry.get("status") == "suspended":
|
|
241
|
+
suspended.append(info)
|
|
242
|
+
active.sort(key=lambda m: m["priority"], reverse=True)
|
|
243
|
+
print(json.dumps({"active": active, "suspended": suspended}, ensure_ascii=False))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def cmd_info(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
247
|
+
"""Show detailed info about a module."""
|
|
248
|
+
module_id = args.module_id
|
|
249
|
+
manifest = _load_manifest(modules_dir, module_id)
|
|
250
|
+
if not manifest:
|
|
251
|
+
_error(f"Module '{module_id}' not found")
|
|
252
|
+
|
|
253
|
+
registry = _load_registry(modules_dir)
|
|
254
|
+
entry = registry.get("modules", {}).get(module_id, {})
|
|
255
|
+
|
|
256
|
+
patterns_path = modules_dir / module_id / "patterns.md"
|
|
257
|
+
patterns_lines = 0
|
|
258
|
+
if patterns_path.exists():
|
|
259
|
+
patterns_lines = len(patterns_path.read_text(encoding="utf-8").splitlines())
|
|
260
|
+
|
|
261
|
+
guidance_path = modules_dir / module_id / "guidance.md"
|
|
262
|
+
guidance_chars = 0
|
|
263
|
+
if guidance_path.exists():
|
|
264
|
+
guidance_chars = len(guidance_path.read_text(encoding="utf-8"))
|
|
265
|
+
|
|
266
|
+
print(json.dumps({
|
|
267
|
+
"id": module_id,
|
|
268
|
+
"manifest": manifest,
|
|
269
|
+
"registry": entry if entry else None,
|
|
270
|
+
"patternsLines": patterns_lines,
|
|
271
|
+
"patternsPath": str(patterns_path),
|
|
272
|
+
"guidanceChars": guidance_chars,
|
|
273
|
+
}, ensure_ascii=False))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def cmd_guidance(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
277
|
+
"""View, set, or clear per-module behavioral guidance."""
|
|
278
|
+
module_id = args.module_id
|
|
279
|
+
module_dir = modules_dir / module_id
|
|
280
|
+
manifest = _load_manifest(modules_dir, module_id)
|
|
281
|
+
if not manifest:
|
|
282
|
+
_error(f"Module '{module_id}' not found (no manifest.json in {module_dir})")
|
|
283
|
+
|
|
284
|
+
guidance_path = module_dir / "guidance.md"
|
|
285
|
+
|
|
286
|
+
if args.clear:
|
|
287
|
+
if guidance_path.exists():
|
|
288
|
+
guidance_path.unlink()
|
|
289
|
+
print(json.dumps({"module": module_id, "guidance": "", "cleared": True}, ensure_ascii=False))
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
if args.set is not None:
|
|
293
|
+
guidance_path.write_text(args.set, encoding="utf-8")
|
|
294
|
+
print(json.dumps({
|
|
295
|
+
"module": module_id,
|
|
296
|
+
"guidanceChars": len(args.set),
|
|
297
|
+
"written": True,
|
|
298
|
+
}, ensure_ascii=False))
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Default: view
|
|
302
|
+
guidance = guidance_path.read_text(encoding="utf-8") if guidance_path.exists() else ""
|
|
303
|
+
print(json.dumps({
|
|
304
|
+
"module": module_id,
|
|
305
|
+
"hasGuidance": bool(guidance),
|
|
306
|
+
"guidance": guidance,
|
|
307
|
+
}, ensure_ascii=False))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# Subcommand: extract (uses LLM)
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
EXTRACT_SYSTEM_PROMPT = """\
|
|
315
|
+
You are a knowledge extraction agent for sinain, a personal AI assistant.
|
|
316
|
+
Your job: given a playbook and recent log history, extract all patterns related to a specific domain.
|
|
317
|
+
|
|
318
|
+
Output format — respond with ONLY a JSON object:
|
|
319
|
+
{
|
|
320
|
+
"established": ["pattern 1 (score > 0.5)", ...],
|
|
321
|
+
"emerging": ["pattern that appeared recently", ...],
|
|
322
|
+
"vocabulary": ["domain-specific term: definition", ...]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Rules:
|
|
326
|
+
- Only include patterns genuinely related to the specified domain
|
|
327
|
+
- "established" = patterns with strong evidence (multiple occurrences, high scores)
|
|
328
|
+
- "emerging" = patterns seen once or twice, plausible but unconfirmed
|
|
329
|
+
- "vocabulary" = domain-specific terms, acronyms, tool names with brief definitions
|
|
330
|
+
- Be specific — cite concrete behaviors, not generic advice
|
|
331
|
+
- If no patterns found for the domain, return empty arrays"""
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def cmd_extract(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
335
|
+
"""Extract domain patterns from playbook + logs using LLM."""
|
|
336
|
+
# Import LLM utilities (only needed for extract)
|
|
337
|
+
try:
|
|
338
|
+
from common import call_llm, extract_json, read_playbook, read_recent_logs, LLMError
|
|
339
|
+
except ImportError:
|
|
340
|
+
_error("Cannot import common.py — run from sinain-koog/ directory or ensure it's on PYTHONPATH")
|
|
341
|
+
|
|
342
|
+
module_id = args.module_id
|
|
343
|
+
domain = args.domain
|
|
344
|
+
memory_dir = args.memory_dir
|
|
345
|
+
min_score = args.min_score
|
|
346
|
+
|
|
347
|
+
# Read source data
|
|
348
|
+
playbook = read_playbook(memory_dir)
|
|
349
|
+
recent_logs = read_recent_logs(memory_dir, days=7)
|
|
350
|
+
|
|
351
|
+
if not playbook and not recent_logs:
|
|
352
|
+
_error("No playbook or logs found — nothing to extract from")
|
|
353
|
+
|
|
354
|
+
# Enrich with knowledge graph context (optional, degrades gracefully)
|
|
355
|
+
kg_context = ""
|
|
356
|
+
try:
|
|
357
|
+
from triple_query import get_related_context
|
|
358
|
+
kg_context = get_related_context(str(memory_dir), [domain], max_chars=1000)
|
|
359
|
+
except Exception:
|
|
360
|
+
pass # triple store unavailable — proceed without
|
|
361
|
+
|
|
362
|
+
# Build user prompt
|
|
363
|
+
parts = [f"## Domain: {domain}"]
|
|
364
|
+
if playbook:
|
|
365
|
+
parts.append(f"\n## Playbook Content\n{playbook}")
|
|
366
|
+
if recent_logs:
|
|
367
|
+
# Summarize logs (keep it compact)
|
|
368
|
+
log_entries = []
|
|
369
|
+
for entry in recent_logs[:20]:
|
|
370
|
+
log_entries.append(json.dumps({
|
|
371
|
+
"ts": entry.get("ts", "?"),
|
|
372
|
+
"signals": entry.get("signals", []),
|
|
373
|
+
"playbookChanges": entry.get("playbookChanges"),
|
|
374
|
+
"output": entry.get("output"),
|
|
375
|
+
}, ensure_ascii=False))
|
|
376
|
+
parts.append(f"\n## Recent Log Entries (last 7 days)\n" + "\n".join(log_entries))
|
|
377
|
+
if kg_context:
|
|
378
|
+
parts.append(f"\n## Knowledge Graph Context\n{kg_context}")
|
|
379
|
+
if min_score:
|
|
380
|
+
parts.append(f"\n## Minimum Score Filter: {min_score}")
|
|
381
|
+
|
|
382
|
+
user_prompt = "\n".join(parts)
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
raw = call_llm(EXTRACT_SYSTEM_PROMPT, user_prompt, script="module_manager", json_mode=True)
|
|
386
|
+
result = extract_json(raw)
|
|
387
|
+
except (ValueError, LLMError) as e:
|
|
388
|
+
_error(f"LLM extraction failed: {e}")
|
|
389
|
+
|
|
390
|
+
established = result.get("established", [])
|
|
391
|
+
emerging = result.get("emerging", [])
|
|
392
|
+
vocabulary = result.get("vocabulary", [])
|
|
393
|
+
|
|
394
|
+
# Create module directory
|
|
395
|
+
module_dir = modules_dir / module_id
|
|
396
|
+
module_dir.mkdir(parents=True, exist_ok=True)
|
|
397
|
+
|
|
398
|
+
# Generate manifest
|
|
399
|
+
manifest = {
|
|
400
|
+
"id": module_id,
|
|
401
|
+
"name": domain,
|
|
402
|
+
"description": f"Auto-extracted patterns for: {domain}",
|
|
403
|
+
"version": "1.0.0",
|
|
404
|
+
"priority": {
|
|
405
|
+
"default": 70,
|
|
406
|
+
"range": [50, 100],
|
|
407
|
+
},
|
|
408
|
+
"triggers": {},
|
|
409
|
+
"locked": False,
|
|
410
|
+
"extractedAt": _now_iso(),
|
|
411
|
+
"source": "module_manager extract",
|
|
412
|
+
}
|
|
413
|
+
(module_dir / "manifest.json").write_text(
|
|
414
|
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Generate patterns.md
|
|
418
|
+
lines = [f"# {domain}\n"]
|
|
419
|
+
if established:
|
|
420
|
+
lines.append("## Established Patterns")
|
|
421
|
+
for p in established:
|
|
422
|
+
lines.append(f"- {p}")
|
|
423
|
+
lines.append("")
|
|
424
|
+
if emerging:
|
|
425
|
+
lines.append("## Emerging Patterns")
|
|
426
|
+
for p in emerging:
|
|
427
|
+
lines.append(f"- {p}")
|
|
428
|
+
lines.append("")
|
|
429
|
+
if vocabulary:
|
|
430
|
+
lines.append("## Domain Vocabulary")
|
|
431
|
+
for v in vocabulary:
|
|
432
|
+
lines.append(f"- {v}")
|
|
433
|
+
lines.append("")
|
|
434
|
+
|
|
435
|
+
(module_dir / "patterns.md").write_text("\n".join(lines), encoding="utf-8")
|
|
436
|
+
|
|
437
|
+
# Register as suspended (user must explicitly activate)
|
|
438
|
+
registry = _load_registry(modules_dir)
|
|
439
|
+
registry.setdefault("modules", {})[module_id] = {
|
|
440
|
+
"status": "suspended",
|
|
441
|
+
"priority": 70,
|
|
442
|
+
"activatedAt": None,
|
|
443
|
+
"lastTriggered": None,
|
|
444
|
+
"locked": False,
|
|
445
|
+
}
|
|
446
|
+
_save_registry(modules_dir, registry)
|
|
447
|
+
|
|
448
|
+
print(json.dumps({
|
|
449
|
+
"extracted": module_id,
|
|
450
|
+
"domain": domain,
|
|
451
|
+
"patternsEstablished": len(established),
|
|
452
|
+
"patternsEmerging": len(emerging),
|
|
453
|
+
"vocabularyTerms": len(vocabulary),
|
|
454
|
+
"modulePath": str(module_dir),
|
|
455
|
+
"status": "suspended",
|
|
456
|
+
"activateWith": f"python3 module_manager.py --modules-dir {modules_dir} activate {module_id}",
|
|
457
|
+
}, ensure_ascii=False))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
# Subcommands: export / import (portable bundles)
|
|
462
|
+
# ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
def cmd_export(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
465
|
+
"""Export a module as a portable .sinain-module.json bundle."""
|
|
466
|
+
module_id = args.module_id
|
|
467
|
+
module_dir = modules_dir / module_id
|
|
468
|
+
|
|
469
|
+
manifest = _load_manifest(modules_dir, module_id)
|
|
470
|
+
if not manifest:
|
|
471
|
+
_error(f"Module '{module_id}' not found (no manifest.json)")
|
|
472
|
+
|
|
473
|
+
# Read patterns
|
|
474
|
+
patterns_path = module_dir / "patterns.md"
|
|
475
|
+
patterns = patterns_path.read_text(encoding="utf-8") if patterns_path.exists() else ""
|
|
476
|
+
|
|
477
|
+
# Read guidance
|
|
478
|
+
guidance_path = module_dir / "guidance.md"
|
|
479
|
+
guidance = guidance_path.read_text(encoding="utf-8") if guidance_path.exists() else ""
|
|
480
|
+
|
|
481
|
+
# Read context files
|
|
482
|
+
context = {}
|
|
483
|
+
context_dir = module_dir / "context"
|
|
484
|
+
if context_dir.is_dir():
|
|
485
|
+
for f in sorted(context_dir.iterdir()):
|
|
486
|
+
if f.is_file():
|
|
487
|
+
try:
|
|
488
|
+
context[f.name] = f.read_text(encoding="utf-8")
|
|
489
|
+
except UnicodeDecodeError:
|
|
490
|
+
pass # skip binary files
|
|
491
|
+
|
|
492
|
+
bundle = {
|
|
493
|
+
"format": "sinain-module-v1",
|
|
494
|
+
"moduleId": module_id,
|
|
495
|
+
"exportedAt": _now_iso(),
|
|
496
|
+
"manifest": manifest,
|
|
497
|
+
"patterns": patterns,
|
|
498
|
+
"guidance": guidance,
|
|
499
|
+
"context": context,
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
output_path = Path(args.output) if args.output else Path(f"{module_id}.sinain-module.json")
|
|
503
|
+
output_path.write_text(json.dumps(bundle, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
504
|
+
|
|
505
|
+
print(json.dumps({
|
|
506
|
+
"exported": module_id,
|
|
507
|
+
"outputPath": str(output_path),
|
|
508
|
+
"patternsChars": len(patterns),
|
|
509
|
+
"guidanceChars": len(guidance),
|
|
510
|
+
"contextFiles": len(context),
|
|
511
|
+
}, ensure_ascii=False))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def cmd_import(modules_dir: Path, args: argparse.Namespace) -> None:
|
|
515
|
+
"""Import a module from a .sinain-module.json bundle."""
|
|
516
|
+
bundle_path = Path(args.bundle)
|
|
517
|
+
if not bundle_path.exists():
|
|
518
|
+
_error(f"Bundle file not found: {bundle_path}")
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
bundle = json.loads(bundle_path.read_text(encoding="utf-8"))
|
|
522
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
523
|
+
_error(f"Invalid bundle file: {e}")
|
|
524
|
+
|
|
525
|
+
# Validate format
|
|
526
|
+
if bundle.get("format") != "sinain-module-v1":
|
|
527
|
+
_error(f"Unknown bundle format: {bundle.get('format')} (expected sinain-module-v1)")
|
|
528
|
+
|
|
529
|
+
module_id = bundle.get("moduleId")
|
|
530
|
+
if not module_id:
|
|
531
|
+
_error("Bundle missing moduleId")
|
|
532
|
+
|
|
533
|
+
module_dir = modules_dir / module_id
|
|
534
|
+
|
|
535
|
+
# Check for existing module
|
|
536
|
+
if module_dir.exists() and not args.force:
|
|
537
|
+
_error(f"Module '{module_id}' already exists. Use --force to overwrite.")
|
|
538
|
+
|
|
539
|
+
# Create module directory
|
|
540
|
+
module_dir.mkdir(parents=True, exist_ok=True)
|
|
541
|
+
|
|
542
|
+
# Write manifest (stamp with import metadata)
|
|
543
|
+
manifest = bundle.get("manifest", {})
|
|
544
|
+
manifest["importedAt"] = _now_iso()
|
|
545
|
+
manifest["source"] = "module_manager import"
|
|
546
|
+
manifest["importedFrom"] = str(bundle_path.name)
|
|
547
|
+
(module_dir / "manifest.json").write_text(
|
|
548
|
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Write patterns
|
|
552
|
+
patterns = bundle.get("patterns", "")
|
|
553
|
+
if patterns:
|
|
554
|
+
(module_dir / "patterns.md").write_text(patterns, encoding="utf-8")
|
|
555
|
+
|
|
556
|
+
# Write guidance
|
|
557
|
+
guidance = bundle.get("guidance", "")
|
|
558
|
+
if guidance:
|
|
559
|
+
(module_dir / "guidance.md").write_text(guidance, encoding="utf-8")
|
|
560
|
+
|
|
561
|
+
# Write context files
|
|
562
|
+
context = bundle.get("context", {})
|
|
563
|
+
if context:
|
|
564
|
+
context_dir = module_dir / "context"
|
|
565
|
+
context_dir.mkdir(exist_ok=True)
|
|
566
|
+
for fname, content in context.items():
|
|
567
|
+
(context_dir / fname).write_text(content, encoding="utf-8")
|
|
568
|
+
|
|
569
|
+
# Register in registry
|
|
570
|
+
registry = _load_registry(modules_dir)
|
|
571
|
+
status = "active" if args.activate else "suspended"
|
|
572
|
+
priority = manifest.get("priority", {}).get("default", 70) if isinstance(manifest.get("priority"), dict) else 70
|
|
573
|
+
registry.setdefault("modules", {})[module_id] = {
|
|
574
|
+
"status": status,
|
|
575
|
+
"priority": priority,
|
|
576
|
+
"activatedAt": _now_iso() if args.activate else None,
|
|
577
|
+
"lastTriggered": None,
|
|
578
|
+
"locked": False,
|
|
579
|
+
}
|
|
580
|
+
_save_registry(modules_dir, registry)
|
|
581
|
+
|
|
582
|
+
# If activating, fire-and-forget KG ingestion (same as cmd_activate)
|
|
583
|
+
if args.activate:
|
|
584
|
+
import subprocess
|
|
585
|
+
try:
|
|
586
|
+
subprocess.run(
|
|
587
|
+
["python3", "triple_ingest.py", "--memory-dir",
|
|
588
|
+
str(modules_dir.parent / "memory"),
|
|
589
|
+
"--ingest-module", module_id, "--modules-dir", str(modules_dir),
|
|
590
|
+
"--embed"],
|
|
591
|
+
capture_output=True, timeout=15,
|
|
592
|
+
cwd=str(Path(__file__).parent),
|
|
593
|
+
)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
|
|
597
|
+
print(json.dumps({
|
|
598
|
+
"imported": module_id,
|
|
599
|
+
"status": status,
|
|
600
|
+
"priority": priority,
|
|
601
|
+
"patternsChars": len(patterns),
|
|
602
|
+
"contextFiles": len(context),
|
|
603
|
+
"modulePath": str(module_dir),
|
|
604
|
+
}, ensure_ascii=False))
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
# CLI entry point
|
|
609
|
+
# ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def main():
|
|
612
|
+
parser = argparse.ArgumentParser(
|
|
613
|
+
description="Sinain Knowledge Module Manager",
|
|
614
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
615
|
+
)
|
|
616
|
+
parser.add_argument(
|
|
617
|
+
"--modules-dir", required=True, type=Path,
|
|
618
|
+
help="Path to modules/ directory",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
622
|
+
|
|
623
|
+
# list
|
|
624
|
+
subparsers.add_parser("list", help="List all modules")
|
|
625
|
+
|
|
626
|
+
# activate
|
|
627
|
+
p_act = subparsers.add_parser("activate", help="Activate a module")
|
|
628
|
+
p_act.add_argument("module_id", help="Module ID")
|
|
629
|
+
p_act.add_argument("--priority", type=int, default=None, help="Set priority (default: from manifest)")
|
|
630
|
+
|
|
631
|
+
# suspend
|
|
632
|
+
p_sus = subparsers.add_parser("suspend", help="Suspend a module")
|
|
633
|
+
p_sus.add_argument("module_id", help="Module ID")
|
|
634
|
+
|
|
635
|
+
# priority
|
|
636
|
+
p_pri = subparsers.add_parser("priority", help="Change module priority")
|
|
637
|
+
p_pri.add_argument("module_id", help="Module ID")
|
|
638
|
+
p_pri.add_argument("new_priority", type=int, help="New priority value")
|
|
639
|
+
|
|
640
|
+
# stack
|
|
641
|
+
subparsers.add_parser("stack", help="Show active module stack")
|
|
642
|
+
|
|
643
|
+
# info
|
|
644
|
+
p_info = subparsers.add_parser("info", help="Show module details")
|
|
645
|
+
p_info.add_argument("module_id", help="Module ID")
|
|
646
|
+
|
|
647
|
+
# guidance
|
|
648
|
+
p_guid = subparsers.add_parser("guidance", help="View/set/clear per-module behavioral guidance")
|
|
649
|
+
p_guid.add_argument("module_id", help="Module ID")
|
|
650
|
+
p_guid.add_argument("--set", default=None, help="Set guidance text")
|
|
651
|
+
p_guid.add_argument("--clear", action="store_true", help="Clear guidance")
|
|
652
|
+
|
|
653
|
+
# extract
|
|
654
|
+
p_ext = subparsers.add_parser("extract", help="Extract domain patterns using LLM")
|
|
655
|
+
p_ext.add_argument("module_id", help="Module ID to create")
|
|
656
|
+
p_ext.add_argument("--domain", required=True, help="Domain description")
|
|
657
|
+
p_ext.add_argument("--memory-dir", required=True, help="Path to memory/ directory")
|
|
658
|
+
p_ext.add_argument("--min-score", type=float, default=0.3, help="Minimum pattern score (default: 0.3)")
|
|
659
|
+
|
|
660
|
+
# export
|
|
661
|
+
p_exp = subparsers.add_parser("export", help="Export module as portable bundle")
|
|
662
|
+
p_exp.add_argument("module_id", help="Module ID to export")
|
|
663
|
+
p_exp.add_argument("--output", default=None, help="Output file path (default: <id>.sinain-module.json)")
|
|
664
|
+
|
|
665
|
+
# import
|
|
666
|
+
p_imp = subparsers.add_parser("import", help="Import module from portable bundle")
|
|
667
|
+
p_imp.add_argument("bundle", help="Path to .sinain-module.json bundle file")
|
|
668
|
+
p_imp.add_argument("--activate", action="store_true", help="Activate module immediately after import")
|
|
669
|
+
p_imp.add_argument("--force", action="store_true", help="Overwrite existing module")
|
|
670
|
+
|
|
671
|
+
args = parser.parse_args()
|
|
672
|
+
|
|
673
|
+
commands = {
|
|
674
|
+
"list": cmd_list,
|
|
675
|
+
"activate": cmd_activate,
|
|
676
|
+
"suspend": cmd_suspend,
|
|
677
|
+
"priority": cmd_priority,
|
|
678
|
+
"stack": cmd_stack,
|
|
679
|
+
"info": cmd_info,
|
|
680
|
+
"guidance": cmd_guidance,
|
|
681
|
+
"extract": cmd_extract,
|
|
682
|
+
"export": cmd_export,
|
|
683
|
+
"import": cmd_import,
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
handler = commands.get(args.command)
|
|
687
|
+
if handler:
|
|
688
|
+
handler(args.modules_dir, args)
|
|
689
|
+
else:
|
|
690
|
+
parser.print_help()
|
|
691
|
+
sys.exit(1)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
if __name__ == "__main__":
|
|
695
|
+
main()
|