@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.
Files changed (53) hide show
  1. package/README.md +183 -0
  2. package/index.ts +2096 -0
  3. package/install.js +155 -0
  4. package/openclaw.plugin.json +59 -0
  5. package/package.json +21 -0
  6. package/sinain-memory/common.py +403 -0
  7. package/sinain-memory/demo_knowledge_transfer.sh +85 -0
  8. package/sinain-memory/embedder.py +268 -0
  9. package/sinain-memory/eval/__init__.py +0 -0
  10. package/sinain-memory/eval/assertions.py +288 -0
  11. package/sinain-memory/eval/judges/__init__.py +0 -0
  12. package/sinain-memory/eval/judges/base_judge.py +61 -0
  13. package/sinain-memory/eval/judges/curation_judge.py +46 -0
  14. package/sinain-memory/eval/judges/insight_judge.py +48 -0
  15. package/sinain-memory/eval/judges/mining_judge.py +42 -0
  16. package/sinain-memory/eval/judges/signal_judge.py +45 -0
  17. package/sinain-memory/eval/schemas.py +247 -0
  18. package/sinain-memory/eval_delta.py +109 -0
  19. package/sinain-memory/eval_reporter.py +642 -0
  20. package/sinain-memory/feedback_analyzer.py +221 -0
  21. package/sinain-memory/git_backup.sh +19 -0
  22. package/sinain-memory/insight_synthesizer.py +181 -0
  23. package/sinain-memory/memory/2026-03-01.md +11 -0
  24. package/sinain-memory/memory/playbook-archive/sinain-playbook-2026-03-01-1418.md +15 -0
  25. package/sinain-memory/memory/playbook-logs/2026-03-01.jsonl +1 -0
  26. package/sinain-memory/memory/sinain-playbook.md +21 -0
  27. package/sinain-memory/memory-config.json +39 -0
  28. package/sinain-memory/memory_miner.py +183 -0
  29. package/sinain-memory/module_manager.py +695 -0
  30. package/sinain-memory/playbook_curator.py +225 -0
  31. package/sinain-memory/requirements.txt +3 -0
  32. package/sinain-memory/signal_analyzer.py +141 -0
  33. package/sinain-memory/test_local.py +402 -0
  34. package/sinain-memory/tests/__init__.py +0 -0
  35. package/sinain-memory/tests/conftest.py +189 -0
  36. package/sinain-memory/tests/test_curator_helpers.py +94 -0
  37. package/sinain-memory/tests/test_embedder.py +210 -0
  38. package/sinain-memory/tests/test_extract_json.py +124 -0
  39. package/sinain-memory/tests/test_feedback_computation.py +121 -0
  40. package/sinain-memory/tests/test_miner_helpers.py +71 -0
  41. package/sinain-memory/tests/test_module_management.py +458 -0
  42. package/sinain-memory/tests/test_parsers.py +96 -0
  43. package/sinain-memory/tests/test_tick_evaluator.py +430 -0
  44. package/sinain-memory/tests/test_triple_extractor.py +255 -0
  45. package/sinain-memory/tests/test_triple_ingest.py +191 -0
  46. package/sinain-memory/tests/test_triple_migrate.py +138 -0
  47. package/sinain-memory/tests/test_triplestore.py +248 -0
  48. package/sinain-memory/tick_evaluator.py +392 -0
  49. package/sinain-memory/triple_extractor.py +402 -0
  50. package/sinain-memory/triple_ingest.py +290 -0
  51. package/sinain-memory/triple_migrate.py +275 -0
  52. package/sinain-memory/triple_query.py +184 -0
  53. package/sinain-memory/triplestore.py +498 -0
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """Phase 3 Step 3: Playbook Curator — archive and update sinain-playbook.md.
3
+
4
+ Archives current playbook, uses LLM to curate (add/prune/promote patterns),
5
+ writes the updated playbook back. Respects the curate directive from feedback_analyzer.
6
+
7
+ Usage:
8
+ python3 playbook_curator.py --memory-dir memory/ --session-summary "..." \
9
+ [--curate-directive normal] [--mining-findings "TEXT"]
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import shutil
15
+ import sys
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ from common import (
20
+ LLMError,
21
+ call_llm_with_fallback,
22
+ extract_json,
23
+ output_json,
24
+ read_effective_playbook,
25
+ read_playbook,
26
+ read_recent_logs,
27
+ )
28
+
29
+ SYSTEM_PROMPT = """\
30
+ You are a playbook curator for a personal AI assistant (sinain).
31
+ Your job: maintain a concise, high-quality playbook of patterns and observations.
32
+
33
+ The playbook has these sections:
34
+ 1. Header comments (mining-index) — DO NOT modify these
35
+ 2. Patterns: "When [context], [approach] worked (score: X)" or failed patterns
36
+ 3. User preference observations
37
+ 4. Stale items with [since: YYYY-MM-DD] or [deferred: YYYY-MM-DD, reason: "..."] tags
38
+ 5. Footer comments (effectiveness) — DO NOT modify these (managed by plugin)
39
+
40
+ Curate rules:
41
+ - ADD new successful patterns from recent sessions
42
+ - ADD failed patterns with reasons
43
+ - ADD user preference observations (recurring topics, tools, rhythms)
44
+ - PRUNE entries older than 7 days without reinforcement
45
+ - PROMOTE patterns seen 3+ times from "observed" to "established"
46
+ - Three Laws: (1) don't remove error-prevention patterns, (2) preserve high-scoring approaches, (3) then evolve
47
+ - Keep under 50 lines — density over completeness
48
+ - DEDUPLICATE: if multiple patterns describe the same concept (e.g., "idle standby" variants), merge them into ONE canonical pattern
49
+ - Before adding: check if the pattern already exists in the Active Module Patterns section — do NOT duplicate module patterns into the base playbook
50
+
51
+ Curate directive controls aggressiveness:
52
+ - "aggressive_prune": effectiveness is low — remove weak/unverified patterns aggressively
53
+ - "normal": balanced add/prune cycle
54
+ - "stability": effectiveness is high — only add patterns with strong evidence (score > 0.5)
55
+ - "insufficient_data": skip effectiveness adjustments, focus on gathering patterns
56
+
57
+ Stale item rules:
58
+ - New fixable patterns get [since: YYYY-MM-DD] tag
59
+ - 48h without change → mandatory Phase 2 action (not your concern, just keep the tag)
60
+ - After 3 actions without resolution → move to [deferred: YYYY-MM-DD, reason: "..."]
61
+ - Max 5 deferred items; if adding 6th, prune oldest deferred
62
+
63
+ Respond with ONLY a JSON object:
64
+ {
65
+ "updatedPlaybook": "full text of updated playbook (body only, between header/footer comments)",
66
+ "changes": {
67
+ "added": ["pattern text", ...],
68
+ "pruned": ["pattern text", ...],
69
+ "promoted": ["pattern text", ...]
70
+ },
71
+ "staleItemActions": ["description of stale item handling", ...]
72
+ }"""
73
+
74
+
75
+ def archive_playbook(memory_dir: str) -> str | None:
76
+ """Archive current playbook to playbook-archive/. Returns archive path or None."""
77
+ src = Path(memory_dir) / "sinain-playbook.md"
78
+ if not src.exists():
79
+ return None
80
+
81
+ archive_dir = Path(memory_dir) / "playbook-archive"
82
+ archive_dir.mkdir(parents=True, exist_ok=True)
83
+
84
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H%M")
85
+ dest = archive_dir / f"sinain-playbook-{ts}.md"
86
+ shutil.copy2(src, dest)
87
+ return str(dest)
88
+
89
+
90
+ def extract_header_footer(playbook: str) -> tuple[str, str, str]:
91
+ """Split playbook into (header_comments, body, footer_comments).
92
+
93
+ Header: lines starting with <!-- mining-index or other top comments
94
+ Footer: lines starting with <!-- effectiveness
95
+ Body: everything between
96
+ """
97
+ lines = playbook.splitlines()
98
+ header_lines = []
99
+ footer_lines = []
100
+ body_lines = []
101
+
102
+ for line in lines:
103
+ stripped = line.strip()
104
+ if stripped.startswith("<!-- mining-index:"):
105
+ header_lines.append(line)
106
+ elif stripped.startswith("<!-- effectiveness:"):
107
+ footer_lines.append(line)
108
+ else:
109
+ body_lines.append(line)
110
+
111
+ return "\n".join(header_lines), "\n".join(body_lines), "\n".join(footer_lines)
112
+
113
+
114
+ def reassemble_playbook(header: str, body: str, footer: str) -> str:
115
+ """Reassemble playbook from header + body + footer, ensuring under 50 lines."""
116
+ parts = []
117
+ if header.strip():
118
+ parts.append(header.strip())
119
+ if body.strip():
120
+ parts.append(body.strip())
121
+ if footer.strip():
122
+ parts.append(footer.strip())
123
+
124
+ text = "\n\n".join(parts)
125
+
126
+ # Enforce 50-line limit on body (header/footer don't count)
127
+ body_lines = body.strip().splitlines()
128
+ if len(body_lines) > 50:
129
+ body = "\n".join(body_lines[:50])
130
+ text = "\n\n".join(filter(None, [header.strip(), body.strip(), footer.strip()]))
131
+
132
+ return text + "\n"
133
+
134
+
135
+ def main():
136
+ parser = argparse.ArgumentParser(description="Phase 3: Playbook curation")
137
+ parser.add_argument("--memory-dir", required=True, help="Path to memory/ directory")
138
+ parser.add_argument("--session-summary", required=True, help="Brief session summary")
139
+ parser.add_argument("--curate-directive", default="normal",
140
+ choices=["aggressive_prune", "normal", "stability", "insufficient_data"],
141
+ help="Curation aggressiveness directive")
142
+ parser.add_argument("--mining-findings", default="", help="Findings from memory_miner (if available)")
143
+ args = parser.parse_args()
144
+
145
+ playbook = read_playbook(args.memory_dir)
146
+ effective_playbook = read_effective_playbook(args.memory_dir)
147
+ logs = read_recent_logs(args.memory_dir, days=7)
148
+
149
+ # Archive before modification
150
+ archive_path = archive_playbook(args.memory_dir)
151
+ if archive_path:
152
+ print(f"[info] Archived playbook to {archive_path}", file=sys.stderr)
153
+
154
+ header, body, footer = extract_header_footer(playbook)
155
+
156
+ # Build LLM prompt
157
+ log_summary = []
158
+ for entry in logs[:10]:
159
+ changes = entry.get("playbookChanges", {})
160
+ output = entry.get("output", {})
161
+ log_summary.append({
162
+ "ts": entry.get("ts", "?"),
163
+ "changes": changes,
164
+ "suggestion": (output.get("suggestion", "") if output else "")[:100],
165
+ "skipped": entry.get("skipped", False),
166
+ })
167
+
168
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
169
+ user_prompt = (
170
+ f"## Current Date\n{today}\n\n"
171
+ f"## Curate Directive\n{args.curate_directive}\n\n"
172
+ f"## Session Summary\n{args.session_summary}\n\n"
173
+ f"## Current Playbook Body\n{body}\n\n"
174
+ f"## Recent Log Entries (last 10)\n{json.dumps(log_summary, indent=2)}\n\n"
175
+ )
176
+ if args.mining_findings:
177
+ user_prompt += f"## Mining Findings\n{args.mining_findings}\n\n"
178
+
179
+ if effective_playbook != playbook:
180
+ user_prompt += f"## Active Module Patterns (DO NOT duplicate these)\n{effective_playbook}\n\n"
181
+
182
+ user_prompt += (
183
+ "Curate the playbook. Return the FULL updated body text and a summary of changes.\n\n"
184
+ "IMPORTANT: Output ONLY the JSON object. No explanation, no reasoning, "
185
+ "no commentary — start your response with { and end with }."
186
+ )
187
+
188
+ try:
189
+ raw = call_llm_with_fallback(SYSTEM_PROMPT, user_prompt, script="playbook_curator", json_mode=True)
190
+ result = extract_json(raw)
191
+ except (ValueError, LLMError) as e:
192
+ print(f"[warn] {e}", file=sys.stderr)
193
+ output_json({
194
+ "changes": {"added": [], "pruned": [], "promoted": []},
195
+ "staleItemActions": [],
196
+ "playbookLines": len(body.splitlines()),
197
+ "error": "LLM response parse failed",
198
+ })
199
+ return
200
+
201
+ # Validate: updatedPlaybook should not be empty and should differ from input
202
+ updated_body = result.get("updatedPlaybook", "")
203
+ if not updated_body or not updated_body.strip():
204
+ print("[warn] LLM returned empty playbook body, keeping original", file=sys.stderr)
205
+ updated_body = body
206
+ elif updated_body.strip() == body.strip():
207
+ print("[info] Curator made no changes", file=sys.stderr)
208
+
209
+ # Write updated playbook
210
+ new_playbook = reassemble_playbook(header, updated_body, footer)
211
+
212
+ playbook_path = Path(args.memory_dir) / "sinain-playbook.md"
213
+ playbook_path.parent.mkdir(parents=True, exist_ok=True)
214
+ playbook_path.write_text(new_playbook, encoding="utf-8")
215
+
216
+ body_lines = updated_body.strip().splitlines()
217
+ output_json({
218
+ "changes": result.get("changes", {"added": [], "pruned": [], "promoted": []}),
219
+ "staleItemActions": result.get("staleItemActions", []),
220
+ "playbookLines": len(body_lines),
221
+ })
222
+
223
+
224
+ if __name__ == "__main__":
225
+ main()
@@ -0,0 +1,3 @@
1
+ requests>=2.28
2
+ numpy>=1.24.0
3
+ sentence-transformers>=2.2.0
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ """Phase 2: Signal Analyzer — detect actionable signals from session context.
3
+
4
+ Reads playbook-logs (last 3 entries) + session summary, uses LLM to identify
5
+ signals and recommend actions (spawn subagent, send tip, or skip).
6
+
7
+ Usage:
8
+ python3 signal_analyzer.py --memory-dir memory/ --session-summary "..." [--idle]
9
+ """
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ from common import (
15
+ LLMError,
16
+ call_llm,
17
+ extract_json,
18
+ output_json,
19
+ read_effective_playbook,
20
+ read_recent_logs,
21
+ )
22
+
23
+ SYSTEM_PROMPT = """\
24
+ You are a signal detection agent for a personal AI assistant system (sinain).
25
+ Your job: scan the user's recent session activity and detect actionable signals.
26
+
27
+ Signal types and recommended actions:
28
+ | Signal | Action |
29
+ |--------|--------|
30
+ | Error or issue repeated in context | sessions_spawn: "Find root cause for: [error]" |
31
+ | New tech/topic being explored | sessions_spawn: "Research [topic]: key findings, best practices, pitfalls" |
32
+ | Clear next action to suggest | telegram_tip: concise suggestion |
33
+ | User stuck (same search/error loop) | sessions_spawn: "Debug [issue]: investigate and propose fix" |
34
+ | No meaningful signal | skip |
35
+
36
+ Signal priority levels:
37
+ - HIGH: Repeated errors, build failures, stuck loops — always act
38
+ - MEDIUM: New topic exploration, workflow opportunity — act if confident
39
+ - LOW: General browsing, social media — skip unless highly relevant
40
+
41
+ Rules:
42
+ - Max 1 recommended action per analysis
43
+ - NEVER repeat an action that appears in recent log entries
44
+ - Prefer depth (spawn research) over breadth (generic tips)
45
+ - If idle (>30 min no activity), set idle=true and skip Phase 2 action
46
+ - Confidence: 0.0-1.0 (only recommend actions with confidence > 0.5)
47
+
48
+ Include a "priority" field ("high"/"medium"/"low") in each signal.
49
+
50
+ Respond with ONLY a JSON object (no markdown, no explanation):
51
+ {
52
+ "signals": [{"description": "signal text", "priority": "high|medium|low"}, ...],
53
+ "recommendedAction": {"action": "sessions_spawn|telegram_tip|skip", "task": "description if not skip", "confidence": 0.7} or null,
54
+ "idle": false
55
+ }"""
56
+
57
+
58
+ def build_user_prompt(
59
+ session_summary: str,
60
+ recent_logs: list[dict],
61
+ playbook: str,
62
+ idle: bool,
63
+ current_time: str | None = None,
64
+ ) -> str:
65
+ parts = []
66
+
67
+ if current_time:
68
+ parts.append(f"## Current Time\n{current_time}")
69
+
70
+ parts.append(f"## Session Summary\n{session_summary}")
71
+
72
+ if idle:
73
+ parts.append("\n## Status: IDLE (>30 min no activity)\nSkip Phase 2 action. Only report if any background signals exist.")
74
+
75
+ if recent_logs:
76
+ log_summary = []
77
+ for entry in recent_logs[:6]:
78
+ actions = entry.get("actionsConsidered", [])
79
+ chosen = [a for a in actions if a.get("chosen")]
80
+ skipped = entry.get("skipped", False)
81
+ log_summary.append(
82
+ f"- ts={entry.get('ts', '?')}, idle={entry.get('idle', '?')}, "
83
+ f"actions_chosen={len(chosen)}, skipped={skipped}"
84
+ )
85
+ for a in chosen:
86
+ log_summary.append(f" -> {a.get('action', '?')}: {a.get('reason', '?')}")
87
+ parts.append(f"\n## Recent Log Entries (last 6)\n" + "\n".join(log_summary))
88
+
89
+ if playbook:
90
+ # Truncate playbook to first 50 lines for context
91
+ lines = playbook.splitlines()[:50]
92
+ parts.append(f"\n## Current Playbook (first 50 lines)\n" + "\n".join(lines))
93
+
94
+ return "\n".join(parts)
95
+
96
+
97
+ def main():
98
+ parser = argparse.ArgumentParser(description="Phase 2: Signal detection")
99
+ parser.add_argument("--memory-dir", required=True, help="Path to memory/ directory")
100
+ parser.add_argument("--session-summary", required=True, help="Brief session summary from main agent")
101
+ parser.add_argument("--idle", action="store_true", help="User is idle (>30 min)")
102
+ parser.add_argument("--current-time", default=None, help="Current local time string (e.g. 'Monday, 2 March 2026, 14:30 (Europe/Berlin)')")
103
+ args = parser.parse_args()
104
+
105
+ playbook = read_effective_playbook(args.memory_dir)
106
+ recent_logs = read_recent_logs(args.memory_dir, days=3)
107
+
108
+ user_prompt = build_user_prompt(
109
+ session_summary=args.session_summary,
110
+ recent_logs=recent_logs,
111
+ playbook=playbook,
112
+ idle=args.idle,
113
+ current_time=args.current_time,
114
+ )
115
+
116
+ try:
117
+ raw = call_llm(SYSTEM_PROMPT, user_prompt, script="signal_analyzer", json_mode=True)
118
+ result = extract_json(raw)
119
+ except (ValueError, LLMError) as e:
120
+ print(f"[warn] {e}", file=sys.stderr)
121
+ result = {
122
+ "signals": [],
123
+ "recommendedAction": None,
124
+ "idle": args.idle,
125
+ }
126
+
127
+ # Suppress low-confidence actions
128
+ action = result.get("recommendedAction")
129
+ if action and isinstance(action, dict):
130
+ confidence = action.get("confidence", 0)
131
+ if confidence < 0.5:
132
+ result["recommendedAction"] = {"action": "skip", "task": None, "confidence": confidence}
133
+ print(f"[info] Suppressed low-confidence action ({confidence})", file=sys.stderr)
134
+
135
+ # Ensure idle flag matches CLI arg
136
+ result["idle"] = args.idle
137
+ output_json(result)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()