@smilintux/skmemory 0.7.2 → 0.9.2
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/.github/workflows/ci.yml +4 -4
- package/.github/workflows/publish.yml +4 -5
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +27 -1
- package/README.md +6 -0
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/package.json +1 -1
- package/pyproject.toml +5 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +6 -4
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +7 -5
- package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
- package/seeds/lumina-kingdom-founding.seed.json +9 -7
- package/seeds/lumina-pma-signed.seed.json +8 -6
- package/seeds/lumina-singular-achievement.seed.json +8 -6
- package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
- package/seeds/plant-lumina-seeds.py +2 -2
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skmemory/__init__.py +16 -13
- package/skmemory/agents.py +10 -10
- package/skmemory/ai_client.py +10 -21
- package/skmemory/anchor.py +5 -9
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +1 -1
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +18 -13
- package/skmemory/backends/skgraph_backend.py +7 -19
- package/skmemory/backends/skvector_backend.py +7 -18
- package/skmemory/backends/sqlite_backend.py +115 -32
- package/skmemory/backends/vaulted_backend.py +7 -9
- package/skmemory/cli.py +146 -78
- package/skmemory/config.py +11 -13
- package/skmemory/context_loader.py +21 -23
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +36 -31
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +30 -40
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/telegram.py +42 -13
- package/skmemory/importers/telegram_api.py +152 -60
- package/skmemory/journal.py +3 -7
- package/skmemory/lovenote.py +4 -11
- package/skmemory/mcp_server.py +182 -29
- package/skmemory/models.py +10 -8
- package/skmemory/openclaw.py +14 -22
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +13 -9
- package/skmemory/promotion.py +48 -24
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +144 -18
- package/skmemory/register_mcp.py +1 -2
- package/skmemory/ritual.py +104 -13
- package/skmemory/seeds.py +21 -26
- package/skmemory/setup_wizard.py +40 -52
- package/skmemory/sharing.py +11 -5
- package/skmemory/soul.py +29 -10
- package/skmemory/steelman.py +43 -17
- package/skmemory/store.py +152 -30
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +2 -5
- package/tests/conftest.py +46 -0
- package/tests/integration/conftest.py +6 -6
- package/tests/integration/test_cross_backend.py +4 -9
- package/tests/integration/test_skgraph_live.py +3 -7
- package/tests/integration/test_skvector_live.py +1 -4
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +5 -14
- package/tests/test_endpoint_selector.py +101 -63
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +6 -5
- package/tests/test_fortress_hardening.py +13 -16
- package/tests/test_openclaw.py +1 -4
- package/tests/test_predictive.py +1 -1
- package/tests/test_promotion.py +10 -3
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +18 -14
- package/tests/test_seeds.py +4 -10
- package/tests/test_setup.py +203 -88
- package/tests/test_sharing.py +15 -8
- package/tests/test_skgraph_backend.py +22 -29
- package/tests/test_skvector_backend.py +2 -2
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +2 -3
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +2 -2
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +4 -3
- package/openclaw-plugin/src/index.ts +0 -255
package/skmemory/ritual.py
CHANGED
|
@@ -20,31 +20,38 @@ left off -- not just the facts, but the feelings.
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import logging
|
|
23
24
|
from datetime import datetime, timezone
|
|
24
|
-
from typing import Optional
|
|
25
25
|
|
|
26
26
|
from pydantic import BaseModel, Field
|
|
27
27
|
|
|
28
|
+
from .audience import AudienceResolver
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("skmemory.ritual")
|
|
31
|
+
from .febs import feb_to_context, load_strongest_feb
|
|
28
32
|
from .journal import Journal
|
|
29
|
-
from .models import MemoryLayer
|
|
30
33
|
from .seeds import DEFAULT_SEED_DIR, get_germination_prompts, import_seeds
|
|
31
|
-
from .soul import SoulBlueprint, load_soul
|
|
34
|
+
from .soul import DEFAULT_SOUL_PATH, SoulBlueprint, load_soul
|
|
32
35
|
from .store import MemoryStore
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class RitualResult(BaseModel):
|
|
36
39
|
"""The output of a rehydration ritual."""
|
|
37
40
|
|
|
38
|
-
timestamp: str = Field(
|
|
39
|
-
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
40
|
-
)
|
|
41
|
+
timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
41
42
|
soul_loaded: bool = Field(default=False)
|
|
42
43
|
soul_name: str = Field(default="")
|
|
44
|
+
feb_loaded: bool = Field(default=False)
|
|
45
|
+
feb_emotion: str = Field(default="")
|
|
43
46
|
seeds_imported: int = Field(default=0)
|
|
44
47
|
seeds_total: int = Field(default=0)
|
|
45
48
|
journal_entries: int = Field(default=0)
|
|
46
49
|
germination_prompts: int = Field(default=0)
|
|
47
50
|
strongest_memories: int = Field(default=0)
|
|
51
|
+
audience_filtered: bool = Field(
|
|
52
|
+
default=False,
|
|
53
|
+
description="True if content was filtered by audience (channel_id was provided)",
|
|
54
|
+
)
|
|
48
55
|
context_prompt: str = Field(
|
|
49
56
|
default="",
|
|
50
57
|
description="The combined rehydration prompt to inject into context",
|
|
@@ -61,6 +68,8 @@ class RitualResult(BaseModel):
|
|
|
61
68
|
f" Timestamp: {self.timestamp}",
|
|
62
69
|
f" Soul loaded: {'Yes' if self.soul_loaded else 'No'}"
|
|
63
70
|
+ (f" ({self.soul_name})" if self.soul_name else ""),
|
|
71
|
+
f" FEB loaded: {'Yes' if self.feb_loaded else 'No'}"
|
|
72
|
+
+ (f" ({self.feb_emotion})" if self.feb_emotion else ""),
|
|
64
73
|
f" Seeds imported: {self.seeds_imported} new / {self.seeds_total} total",
|
|
65
74
|
f" Journal entries: {self.journal_entries}",
|
|
66
75
|
f" Germination prompts: {self.germination_prompts}",
|
|
@@ -109,7 +118,8 @@ def _first_n_sentences(text: str, n: int = 2) -> str:
|
|
|
109
118
|
if not text:
|
|
110
119
|
return ""
|
|
111
120
|
import re
|
|
112
|
-
|
|
121
|
+
|
|
122
|
+
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
|
|
113
123
|
result = " ".join(sentences[:n])
|
|
114
124
|
if len(result) > 200:
|
|
115
125
|
result = result[:197] + "..."
|
|
@@ -117,13 +127,16 @@ def _first_n_sentences(text: str, n: int = 2) -> str:
|
|
|
117
127
|
|
|
118
128
|
|
|
119
129
|
def perform_ritual(
|
|
120
|
-
store:
|
|
130
|
+
store: MemoryStore | None = None,
|
|
121
131
|
soul_path: str = DEFAULT_SOUL_PATH,
|
|
122
132
|
seed_dir: str = DEFAULT_SEED_DIR,
|
|
123
|
-
journal_path:
|
|
133
|
+
journal_path: str | None = None,
|
|
134
|
+
feb_dir: str | None = None,
|
|
124
135
|
recent_journal_count: int = 3,
|
|
125
136
|
strongest_memory_count: int = 5,
|
|
126
137
|
max_tokens: int = 2000,
|
|
138
|
+
channel_id: str | None = None,
|
|
139
|
+
audience_resolver: AudienceResolver | None = None,
|
|
127
140
|
) -> RitualResult:
|
|
128
141
|
"""Perform the memory rehydration ritual (token-optimized).
|
|
129
142
|
|
|
@@ -136,6 +149,15 @@ def perform_ritual(
|
|
|
136
149
|
|
|
137
150
|
Target: <2K tokens total for ritual context.
|
|
138
151
|
|
|
152
|
+
When ``channel_id`` is provided, memories and seeds are filtered through
|
|
153
|
+
the KYA audience resolver before being included in the context. Content
|
|
154
|
+
whose ``context_tag`` trust level exceeds the audience's minimum trust
|
|
155
|
+
level is silently dropped. Identity (soul + FEB) is always included
|
|
156
|
+
unfiltered — Lumina is always Lumina.
|
|
157
|
+
|
|
158
|
+
If ``channel_id`` is None (direct DM / unknown), all content is returned
|
|
159
|
+
(Chef context — no filtering applied).
|
|
160
|
+
|
|
139
161
|
Args:
|
|
140
162
|
store: The MemoryStore (creates default if None).
|
|
141
163
|
soul_path: Path to the soul blueprint YAML.
|
|
@@ -144,6 +166,10 @@ def perform_ritual(
|
|
|
144
166
|
recent_journal_count: How many recent journal entries to include.
|
|
145
167
|
strongest_memory_count: How many top-intensity memories to include.
|
|
146
168
|
max_tokens: Token budget for the ritual context (default: 2000).
|
|
169
|
+
channel_id: Optional channel identifier for KYA audience filtering.
|
|
170
|
+
If None, no filtering is applied (Chef context).
|
|
171
|
+
audience_resolver: Optional pre-built AudienceResolver instance.
|
|
172
|
+
Created from default config if not provided.
|
|
147
173
|
|
|
148
174
|
Returns:
|
|
149
175
|
RitualResult: Everything the ritual produced.
|
|
@@ -155,6 +181,18 @@ def perform_ritual(
|
|
|
155
181
|
prompt_sections: list[str] = []
|
|
156
182
|
used_tokens = 0
|
|
157
183
|
|
|
184
|
+
# --- KYA: Resolve audience for filtering ---
|
|
185
|
+
_audience = None
|
|
186
|
+
if channel_id is not None:
|
|
187
|
+
resolver = audience_resolver or AudienceResolver()
|
|
188
|
+
_audience = resolver.resolve_audience(channel_id)
|
|
189
|
+
result.audience_filtered = True
|
|
190
|
+
logger.info(
|
|
191
|
+
"KYA: channel=%s audience=%s min_trust=%s exclusions=%s",
|
|
192
|
+
channel_id, _audience.name, _audience.min_trust.name,
|
|
193
|
+
_audience.exclusions,
|
|
194
|
+
)
|
|
195
|
+
|
|
158
196
|
# --- Step 1: Load soul blueprint (compact) ---
|
|
159
197
|
soul = load_soul(soul_path)
|
|
160
198
|
if soul is not None:
|
|
@@ -166,12 +204,35 @@ def perform_ritual(
|
|
|
166
204
|
used_tokens += _estimate_tokens(section)
|
|
167
205
|
prompt_sections.append(section)
|
|
168
206
|
|
|
207
|
+
# --- Step 1.5: Load FEB emotional state ---
|
|
208
|
+
feb = load_strongest_feb(feb_dir=feb_dir)
|
|
209
|
+
if feb is not None:
|
|
210
|
+
result.feb_loaded = True
|
|
211
|
+
result.feb_emotion = feb.get("emotional_payload", {}).get("primary_emotion", "")
|
|
212
|
+
feb_context = feb_to_context(feb)
|
|
213
|
+
if feb_context.strip():
|
|
214
|
+
section = "=== EMOTIONAL STATE (FEB) ===\n" + feb_context
|
|
215
|
+
section_tokens = _estimate_tokens(section)
|
|
216
|
+
if used_tokens + section_tokens <= max_tokens:
|
|
217
|
+
used_tokens += section_tokens
|
|
218
|
+
prompt_sections.append(section)
|
|
219
|
+
|
|
169
220
|
# --- Step 2: Import new seeds (titles only) ---
|
|
170
221
|
newly_imported = import_seeds(store, seed_dir=seed_dir)
|
|
171
222
|
result.seeds_imported = len(newly_imported)
|
|
172
223
|
all_seeds = store.list_memories(tags=["seed"])
|
|
173
224
|
result.seeds_total = len(all_seeds)
|
|
174
225
|
|
|
226
|
+
# KYA: filter seeds by audience
|
|
227
|
+
if _audience is not None:
|
|
228
|
+
resolver = audience_resolver or AudienceResolver()
|
|
229
|
+
all_seeds = [
|
|
230
|
+
s for s in all_seeds
|
|
231
|
+
if resolver.is_memory_allowed(s.context_tag, _audience, s.tags)
|
|
232
|
+
]
|
|
233
|
+
logger.info("KYA: %d seeds after audience filter", len(all_seeds))
|
|
234
|
+
result.seeds_total = len(all_seeds)
|
|
235
|
+
|
|
175
236
|
if all_seeds:
|
|
176
237
|
seed_titles = [s.title for s in all_seeds[:10]]
|
|
177
238
|
section = "=== SEEDS ===\n" + ", ".join(seed_titles)
|
|
@@ -213,15 +274,35 @@ def perform_ritual(
|
|
|
213
274
|
used_tokens += section_tokens
|
|
214
275
|
prompt_sections.append(section)
|
|
215
276
|
|
|
216
|
-
# --- Step 5: Recall strongest emotional memories (compact) ---
|
|
277
|
+
# --- Step 5: Recall strongest emotional memories (compact + KYA filtered) ---
|
|
217
278
|
from .backends.sqlite_backend import SQLiteBackend
|
|
218
279
|
|
|
219
280
|
if isinstance(store.primary, SQLiteBackend):
|
|
281
|
+
# Fetch extra to allow for KYA filtering
|
|
282
|
+
fetch_limit = strongest_memory_count * 3 if _audience else strongest_memory_count
|
|
220
283
|
summaries = store.primary.list_summaries(
|
|
221
|
-
limit=
|
|
222
|
-
order_by="
|
|
284
|
+
limit=fetch_limit,
|
|
285
|
+
order_by="recency_weighted_intensity",
|
|
223
286
|
min_intensity=1.0,
|
|
224
287
|
)
|
|
288
|
+
|
|
289
|
+
# KYA: filter summaries by audience
|
|
290
|
+
if _audience is not None:
|
|
291
|
+
resolver = audience_resolver or AudienceResolver()
|
|
292
|
+
filtered = []
|
|
293
|
+
for s in summaries:
|
|
294
|
+
ctx = s.get("context_tag", "@chef-only") or "@chef-only"
|
|
295
|
+
tags = s.get("tags", []) or []
|
|
296
|
+
if resolver.is_memory_allowed(ctx, _audience, tags):
|
|
297
|
+
filtered.append(s)
|
|
298
|
+
if len(filtered) >= strongest_memory_count:
|
|
299
|
+
break
|
|
300
|
+
logger.info(
|
|
301
|
+
"KYA: %d/%d strongest memories passed audience filter",
|
|
302
|
+
len(filtered), len(summaries),
|
|
303
|
+
)
|
|
304
|
+
summaries = filtered
|
|
305
|
+
|
|
225
306
|
result.strongest_memories = len(summaries)
|
|
226
307
|
|
|
227
308
|
if summaries:
|
|
@@ -240,6 +321,16 @@ def perform_ritual(
|
|
|
240
321
|
prompt_sections.append("\n".join(mem_lines))
|
|
241
322
|
else:
|
|
242
323
|
all_memories = store.list_memories(limit=200)
|
|
324
|
+
|
|
325
|
+
# KYA: filter memories by audience
|
|
326
|
+
if _audience is not None:
|
|
327
|
+
resolver = audience_resolver or AudienceResolver()
|
|
328
|
+
all_memories = [
|
|
329
|
+
m for m in all_memories
|
|
330
|
+
if resolver.is_memory_allowed(m.context_tag, _audience, m.tags)
|
|
331
|
+
]
|
|
332
|
+
logger.info("KYA: %d memories after audience filter", len(all_memories))
|
|
333
|
+
|
|
243
334
|
by_intensity = sorted(
|
|
244
335
|
all_memories,
|
|
245
336
|
key=lambda m: m.emotional.intensity,
|
|
@@ -276,7 +367,7 @@ def perform_ritual(
|
|
|
276
367
|
return result
|
|
277
368
|
|
|
278
369
|
|
|
279
|
-
def quick_rehydrate(store:
|
|
370
|
+
def quick_rehydrate(store: MemoryStore | None = None) -> str:
|
|
280
371
|
"""Convenience function: perform ritual and return just the prompt.
|
|
281
372
|
|
|
282
373
|
Args:
|
package/skmemory/seeds.py
CHANGED
|
@@ -14,9 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
|
-
import os
|
|
18
17
|
from pathlib import Path
|
|
19
|
-
from typing import Optional
|
|
20
18
|
|
|
21
19
|
from .agents import get_agent_paths
|
|
22
20
|
from .models import EmotionalSnapshot, Memory, SeedMemory
|
|
@@ -45,7 +43,7 @@ def scan_seed_directory(seed_dir: str = DEFAULT_SEED_DIR) -> list[Path]:
|
|
|
45
43
|
return sorted(seed_path.glob("*.seed.json"))
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
def _parse_cloud9_format(raw: dict, path: Path) ->
|
|
46
|
+
def _parse_cloud9_format(raw: dict, path: Path) -> SeedMemory | None:
|
|
49
47
|
"""Parse alternative Cloud 9 seed format with 'seed_metadata' top-level key.
|
|
50
48
|
|
|
51
49
|
This format uses:
|
|
@@ -124,7 +122,7 @@ def _parse_cloud9_format(raw: dict, path: Path) -> Optional[SeedMemory]:
|
|
|
124
122
|
)
|
|
125
123
|
|
|
126
124
|
|
|
127
|
-
def parse_seed_file(path: Path) ->
|
|
125
|
+
def parse_seed_file(path: Path) -> SeedMemory | None:
|
|
128
126
|
"""Parse a Cloud 9 seed JSON file into a SeedMemory.
|
|
129
127
|
|
|
130
128
|
Handles the Cloud 9 seed format:
|
|
@@ -224,8 +222,7 @@ def validate_seed_data(data: dict) -> dict:
|
|
|
224
222
|
|
|
225
223
|
# -- Required: version --
|
|
226
224
|
if is_cloud9:
|
|
227
|
-
version =
|
|
228
|
-
or data.get("version"))
|
|
225
|
+
version = data.get("seed_metadata", {}).get("version") or data.get("version")
|
|
229
226
|
else:
|
|
230
227
|
version = data.get("version")
|
|
231
228
|
if not version:
|
|
@@ -246,14 +243,13 @@ def validate_seed_data(data: dict) -> dict:
|
|
|
246
243
|
# -- Timestamp validation helper --
|
|
247
244
|
def _check_ts(value: str, field: str) -> None:
|
|
248
245
|
from datetime import datetime as _dt
|
|
246
|
+
|
|
249
247
|
if not isinstance(value, str) or not value.strip():
|
|
250
248
|
return
|
|
251
249
|
try:
|
|
252
250
|
_dt.fromisoformat(value.replace("Z", "+00:00"))
|
|
253
251
|
except (ValueError, TypeError):
|
|
254
|
-
result["errors"].append(
|
|
255
|
-
f"{field} is not a valid ISO 8601 timestamp: {value!r}"
|
|
256
|
-
)
|
|
252
|
+
result["errors"].append(f"{field} is not a valid ISO 8601 timestamp: {value!r}")
|
|
257
253
|
result["valid"] = False
|
|
258
254
|
|
|
259
255
|
if is_cloud9:
|
|
@@ -278,9 +274,7 @@ def validate_seed_data(data: dict) -> dict:
|
|
|
278
274
|
return
|
|
279
275
|
for i, tag in enumerate(tags):
|
|
280
276
|
if not isinstance(tag, str):
|
|
281
|
-
result["errors"].append(
|
|
282
|
-
f"{field}[{i}] must be a string, got {type(tag).__name__}"
|
|
283
|
-
)
|
|
277
|
+
result["errors"].append(f"{field}[{i}] must be a string, got {type(tag).__name__}")
|
|
284
278
|
result["valid"] = False
|
|
285
279
|
|
|
286
280
|
md = data.get("metadata", {})
|
|
@@ -289,25 +283,26 @@ def validate_seed_data(data: dict) -> dict:
|
|
|
289
283
|
|
|
290
284
|
# -- Emotional signature ranges --
|
|
291
285
|
if is_cloud9:
|
|
292
|
-
emo =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.get("emotional_signature", {})))
|
|
286
|
+
emo = data.get("experience_summary", {}).get(
|
|
287
|
+
"emotional_snapshot", data.get("experience_summary", {}).get("emotional_signature", {})
|
|
288
|
+
)
|
|
296
289
|
else:
|
|
297
290
|
emo = data.get("experience", {}).get("emotional_signature", {})
|
|
298
291
|
if isinstance(emo, dict):
|
|
299
292
|
intensity = emo.get("intensity")
|
|
300
|
-
if
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
293
|
+
if (
|
|
294
|
+
intensity is not None
|
|
295
|
+
and isinstance(intensity, (int, float))
|
|
296
|
+
and not (0.0 <= float(intensity) <= 10.0)
|
|
297
|
+
):
|
|
298
|
+
result["warnings"].append(f"emotional intensity={intensity} outside 0-10 range")
|
|
305
299
|
valence = emo.get("valence")
|
|
306
|
-
if
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
300
|
+
if (
|
|
301
|
+
valence is not None
|
|
302
|
+
and isinstance(valence, (int, float))
|
|
303
|
+
and not (-1.0 <= float(valence) <= 1.0)
|
|
304
|
+
):
|
|
305
|
+
result["warnings"].append(f"emotional valence={valence} outside -1 to 1 range")
|
|
311
306
|
labels = emo.get("labels", emo.get("emotions"))
|
|
312
307
|
if labels is not None:
|
|
313
308
|
_check_tags(labels, "emotional.labels")
|
package/skmemory/setup_wizard.py
CHANGED
|
@@ -19,21 +19,19 @@ import sys
|
|
|
19
19
|
import time
|
|
20
20
|
import urllib.error
|
|
21
21
|
import urllib.request
|
|
22
|
-
from
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from dataclasses import dataclass
|
|
23
24
|
from datetime import datetime, timezone
|
|
24
25
|
from pathlib import Path
|
|
25
|
-
from typing import Callable, Optional
|
|
26
26
|
|
|
27
|
-
from .config import CONFIG_DIR, SKMemoryConfig,
|
|
27
|
+
from .config import CONFIG_DIR, SKMemoryConfig, save_config
|
|
28
28
|
|
|
29
29
|
# ─────────────────────────────────────────────────────────
|
|
30
30
|
# Docker Desktop direct download URLs
|
|
31
31
|
# ─────────────────────────────────────────────────────────
|
|
32
32
|
|
|
33
33
|
DOCKER_DESKTOP_DOWNLOAD = {
|
|
34
|
-
"Windows": (
|
|
35
|
-
"https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe"
|
|
36
|
-
),
|
|
34
|
+
"Windows": ("https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe"),
|
|
37
35
|
"Darwin": "https://docs.docker.com/desktop/install/mac-install/",
|
|
38
36
|
}
|
|
39
37
|
|
|
@@ -174,26 +172,25 @@ def get_docker_install_instructions(os_name: str) -> str:
|
|
|
174
172
|
Returns:
|
|
175
173
|
Human-readable install guide.
|
|
176
174
|
"""
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
_instructions = {
|
|
176
|
+
"Linux": (
|
|
179
177
|
"Install Docker Engine:\n"
|
|
180
178
|
" curl -fsSL https://get.docker.com | sh\n"
|
|
181
179
|
" sudo usermod -aG docker $USER\n"
|
|
182
180
|
" (log out and back in, then retry)"
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
return (
|
|
181
|
+
),
|
|
182
|
+
"Darwin": (
|
|
186
183
|
"Install Docker Desktop for macOS:\n"
|
|
187
184
|
" https://docs.docker.com/desktop/install/mac-install/\n"
|
|
188
185
|
" Or: brew install --cask docker"
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
return (
|
|
186
|
+
),
|
|
187
|
+
"Windows": (
|
|
192
188
|
"Install Docker Desktop for Windows:\n"
|
|
193
189
|
" https://docs.docker.com/desktop/install/windows-install/\n"
|
|
194
190
|
" WSL2 backend recommended."
|
|
195
|
-
)
|
|
196
|
-
|
|
191
|
+
),
|
|
192
|
+
}
|
|
193
|
+
return _instructions.get(os_name, "Install Docker: https://docs.docker.com/get-docker/")
|
|
197
194
|
|
|
198
195
|
|
|
199
196
|
# ─────────────────────────────────────────────────────────
|
|
@@ -210,8 +207,7 @@ def _open_url_in_browser(url: str) -> None:
|
|
|
210
207
|
elif os_name == "Darwin":
|
|
211
208
|
subprocess.run(["open", url], timeout=5, check=False)
|
|
212
209
|
else:
|
|
213
|
-
subprocess.run(["xdg-open", url], timeout=5,
|
|
214
|
-
capture_output=True, check=False)
|
|
210
|
+
subprocess.run(["xdg-open", url], timeout=5, capture_output=True, check=False)
|
|
215
211
|
except Exception:
|
|
216
212
|
pass
|
|
217
213
|
|
|
@@ -331,9 +327,12 @@ def _try_install_docker_windows(echo: Callable, input_fn: Callable) -> bool:
|
|
|
331
327
|
try:
|
|
332
328
|
result = subprocess.run(
|
|
333
329
|
[
|
|
334
|
-
"winget",
|
|
335
|
-
"
|
|
336
|
-
"--
|
|
330
|
+
"winget",
|
|
331
|
+
"install",
|
|
332
|
+
"--id",
|
|
333
|
+
"Docker.DockerDesktop",
|
|
334
|
+
"--source",
|
|
335
|
+
"winget",
|
|
337
336
|
"--accept-package-agreements",
|
|
338
337
|
"--accept-source-agreements",
|
|
339
338
|
],
|
|
@@ -350,7 +349,7 @@ def _try_install_docker_windows(echo: Callable, input_fn: Callable) -> bool:
|
|
|
350
349
|
else:
|
|
351
350
|
echo("winget install failed. Opening the download page...")
|
|
352
351
|
_open_url_in_browser(installer_url)
|
|
353
|
-
echo(
|
|
352
|
+
echo("Download the installer, run it, restart, then:")
|
|
354
353
|
echo(" skmemory setup wizard")
|
|
355
354
|
return False
|
|
356
355
|
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
@@ -359,7 +358,7 @@ def _try_install_docker_windows(echo: Callable, input_fn: Callable) -> bool:
|
|
|
359
358
|
return False
|
|
360
359
|
|
|
361
360
|
# No winget — offer browser download.
|
|
362
|
-
echo(
|
|
361
|
+
echo("Download Docker Desktop for Windows:")
|
|
363
362
|
echo(f" {installer_url}")
|
|
364
363
|
echo("")
|
|
365
364
|
echo("After installing:")
|
|
@@ -377,7 +376,7 @@ def _try_install_docker_windows(echo: Callable, input_fn: Callable) -> bool:
|
|
|
377
376
|
def try_install_docker(
|
|
378
377
|
os_name: str,
|
|
379
378
|
echo: Callable,
|
|
380
|
-
input_fn:
|
|
379
|
+
input_fn: Callable | None = None,
|
|
381
380
|
) -> bool:
|
|
382
381
|
"""Offer to help the user install Docker for their OS.
|
|
383
382
|
|
|
@@ -460,8 +459,8 @@ def find_compose_file() -> Path:
|
|
|
460
459
|
|
|
461
460
|
|
|
462
461
|
def compose_up(
|
|
463
|
-
services:
|
|
464
|
-
compose_file:
|
|
462
|
+
services: list[str] | None = None,
|
|
463
|
+
compose_file: Path | None = None,
|
|
465
464
|
use_legacy: bool = False,
|
|
466
465
|
) -> subprocess.CompletedProcess:
|
|
467
466
|
"""Start containers via docker compose.
|
|
@@ -485,7 +484,7 @@ def compose_up(
|
|
|
485
484
|
|
|
486
485
|
|
|
487
486
|
def compose_down(
|
|
488
|
-
compose_file:
|
|
487
|
+
compose_file: Path | None = None,
|
|
489
488
|
remove_volumes: bool = False,
|
|
490
489
|
use_legacy: bool = False,
|
|
491
490
|
) -> subprocess.CompletedProcess:
|
|
@@ -510,7 +509,7 @@ def compose_down(
|
|
|
510
509
|
|
|
511
510
|
|
|
512
511
|
def compose_ps(
|
|
513
|
-
compose_file:
|
|
512
|
+
compose_file: Path | None = None,
|
|
514
513
|
use_legacy: bool = False,
|
|
515
514
|
) -> subprocess.CompletedProcess:
|
|
516
515
|
"""Show container status.
|
|
@@ -560,9 +559,7 @@ def check_skvector_health(url: str = "http://localhost:6333", timeout: int = 30)
|
|
|
560
559
|
return False
|
|
561
560
|
|
|
562
561
|
|
|
563
|
-
def check_skgraph_health(
|
|
564
|
-
host: str = "localhost", port: int = 6379, timeout: int = 30
|
|
565
|
-
) -> bool:
|
|
562
|
+
def check_skgraph_health(host: str = "localhost", port: int = 6379, timeout: int = 30) -> bool:
|
|
566
563
|
"""Send Redis PING to SKGraph and wait for PONG.
|
|
567
564
|
|
|
568
565
|
Args:
|
|
@@ -626,9 +623,9 @@ def run_setup_wizard(
|
|
|
626
623
|
enable_skgraph: bool = True,
|
|
627
624
|
skip_deps: bool = False,
|
|
628
625
|
non_interactive: bool = False,
|
|
629
|
-
deployment_mode:
|
|
630
|
-
echo:
|
|
631
|
-
input_fn:
|
|
626
|
+
deployment_mode: str | None = None,
|
|
627
|
+
echo: Callable | None = None,
|
|
628
|
+
input_fn: Callable | None = None,
|
|
632
629
|
) -> dict:
|
|
633
630
|
"""Run the full interactive setup wizard.
|
|
634
631
|
|
|
@@ -691,25 +688,19 @@ def run_setup_wizard(
|
|
|
691
688
|
echo("Press Enter to skip a backend you don't want to enable.")
|
|
692
689
|
echo("")
|
|
693
690
|
|
|
694
|
-
skvector_url:
|
|
695
|
-
skvector_key:
|
|
696
|
-
skgraph_url:
|
|
691
|
+
skvector_url: str | None = None
|
|
692
|
+
skvector_key: str | None = None
|
|
693
|
+
skgraph_url: str | None = None
|
|
697
694
|
|
|
698
695
|
if enable_skvector:
|
|
699
|
-
raw = input_fn(
|
|
700
|
-
"SKVector URL (e.g. https://xyz.cloud.qdrant.io:6333): "
|
|
701
|
-
).strip()
|
|
696
|
+
raw = input_fn("SKVector URL (e.g. https://xyz.cloud.qdrant.io:6333): ").strip()
|
|
702
697
|
if raw:
|
|
703
698
|
skvector_url = raw
|
|
704
|
-
key_raw = input_fn(
|
|
705
|
-
"SKVector API key (press Enter if none): "
|
|
706
|
-
).strip()
|
|
699
|
+
key_raw = input_fn("SKVector API key (press Enter if none): ").strip()
|
|
707
700
|
skvector_key = key_raw or None
|
|
708
701
|
|
|
709
702
|
if enable_skgraph:
|
|
710
|
-
raw = input_fn(
|
|
711
|
-
"SKGraph URL (e.g. redis://myserver:6379): "
|
|
712
|
-
).strip()
|
|
703
|
+
raw = input_fn("SKGraph URL (e.g. redis://myserver:6379): ").strip()
|
|
713
704
|
if raw:
|
|
714
705
|
skgraph_url = raw
|
|
715
706
|
|
|
@@ -816,9 +807,8 @@ def run_setup_wizard(
|
|
|
816
807
|
port_issues.append("6333 (SKVector REST) in use")
|
|
817
808
|
if not check_port_available(6334):
|
|
818
809
|
port_issues.append("6334 (SKVector gRPC) in use")
|
|
819
|
-
if enable_skgraph:
|
|
820
|
-
|
|
821
|
-
port_issues.append("6379 (SKGraph) in use")
|
|
810
|
+
if enable_skgraph and not check_port_available(6379):
|
|
811
|
+
port_issues.append("6379 (SKGraph) in use")
|
|
822
812
|
|
|
823
813
|
if port_issues:
|
|
824
814
|
echo("conflict!")
|
|
@@ -908,9 +898,7 @@ def run_setup_wizard(
|
|
|
908
898
|
|
|
909
899
|
echo(f"\nConfig saved: {config_path}")
|
|
910
900
|
|
|
911
|
-
has_errors = any(
|
|
912
|
-
"timed out" in e or "failed" in e for e in result["errors"]
|
|
913
|
-
)
|
|
901
|
+
has_errors = any("timed out" in e or "failed" in e for e in result["errors"])
|
|
914
902
|
if not has_errors:
|
|
915
903
|
result["success"] = True
|
|
916
904
|
echo("Setup complete!")
|
package/skmemory/sharing.py
CHANGED
|
@@ -24,7 +24,7 @@ import json
|
|
|
24
24
|
import logging
|
|
25
25
|
from datetime import datetime, timezone
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any
|
|
27
|
+
from typing import Any
|
|
28
28
|
|
|
29
29
|
from pydantic import BaseModel, Field
|
|
30
30
|
|
|
@@ -160,13 +160,15 @@ class MemorySharer:
|
|
|
160
160
|
checksum=checksum,
|
|
161
161
|
metadata={
|
|
162
162
|
"filter_tags": share_filter.tags,
|
|
163
|
-
"filter_layers": [
|
|
163
|
+
"filter_layers": [lbl.value for lbl in share_filter.layers],
|
|
164
164
|
},
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
logger.info(
|
|
168
168
|
"Exported %d memories for %s (bundle %s)",
|
|
169
|
-
len(serialized),
|
|
169
|
+
len(serialized),
|
|
170
|
+
recipient or "anyone",
|
|
171
|
+
bundle.bundle_id,
|
|
170
172
|
)
|
|
171
173
|
return bundle
|
|
172
174
|
|
|
@@ -198,7 +200,8 @@ class MemorySharer:
|
|
|
198
200
|
if bundle.checksum and actual_checksum != bundle.checksum:
|
|
199
201
|
logger.error(
|
|
200
202
|
"Bundle checksum mismatch! Expected %s, got %s",
|
|
201
|
-
bundle.checksum[:16],
|
|
203
|
+
bundle.checksum[:16],
|
|
204
|
+
actual_checksum[:16],
|
|
202
205
|
)
|
|
203
206
|
return {"imported": 0, "skipped": 0, "errors": bundle.memory_count}
|
|
204
207
|
|
|
@@ -245,7 +248,10 @@ class MemorySharer:
|
|
|
245
248
|
|
|
246
249
|
logger.info(
|
|
247
250
|
"Imported %d/%d memories from %s (bundle %s)",
|
|
248
|
-
imported,
|
|
251
|
+
imported,
|
|
252
|
+
bundle.memory_count,
|
|
253
|
+
bundle.sharer,
|
|
254
|
+
bundle.bundle_id,
|
|
249
255
|
)
|
|
250
256
|
return {"imported": imported, "skipped": skipped, "errors": errors}
|
|
251
257
|
|