nyx-memory 3.4.2__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.
Files changed (74) hide show
  1. nexsandglass/__init__.py +113 -0
  2. nexsandglass/core/__init__.py +67 -0
  3. nexsandglass/core/embedding_provider.py +176 -0
  4. nexsandglass/core/emotion_vocab.py +186 -0
  5. nexsandglass/core/l0_buffer.py +75 -0
  6. nexsandglass/core/llm_extract.py +81 -0
  7. nexsandglass/core/memory_provider.py +591 -0
  8. nexsandglass/core/sandglass.py +75 -0
  9. nexsandglass/core/sandglass_archive.py +149 -0
  10. nexsandglass/core/sandglass_log.py +124 -0
  11. nexsandglass/core/sandglass_paths.py +41 -0
  12. nexsandglass/core/sandglass_sqlite.py +163 -0
  13. nexsandglass/core/search_router.py +304 -0
  14. nexsandglass/core/vector_search.py +94 -0
  15. nexsandglass/core/vector_store.py +227 -0
  16. nexsandglass/engram/__init__.py +126 -0
  17. nexsandglass/engram/bridge.py +272 -0
  18. nexsandglass/engram/context.py +194 -0
  19. nexsandglass/engram/decay.py +278 -0
  20. nexsandglass/engram/dream_pipeline.py +242 -0
  21. nexsandglass/engram/evolve.py +143 -0
  22. nexsandglass/engram/loops/__init__.py +67 -0
  23. nexsandglass/engram/loops/dream_engram.py +160 -0
  24. nexsandglass/engram/loops/fact_thread.py +195 -0
  25. nexsandglass/engram/loops/persona_ctx.py +137 -0
  26. nexsandglass/engram/loops/recall_writer.py +173 -0
  27. nexsandglass/engram/loops/temporal_fact.py +272 -0
  28. nexsandglass/engram/types.py +295 -0
  29. nexsandglass/engram/writer.py +240 -0
  30. nexsandglass/features/__init__.py +112 -0
  31. nexsandglass/features/decision_particles.py +715 -0
  32. nexsandglass/features/multi_analysis.py +264 -0
  33. nexsandglass/features/nightwatch.py +151 -0
  34. nexsandglass/features/pulse.py +328 -0
  35. nexsandglass/features/sandglass_chroma.py +103 -0
  36. nexsandglass/features/sandglass_think.py +1505 -0
  37. nexsandglass/features/sandglass_vault.py +561 -0
  38. nexsandglass/features/shadow_sand.py +243 -0
  39. nexsandglass/features/soul_diff.py +112 -0
  40. nexsandglass/features/weavethread.py +328 -0
  41. nexsandglass/interfaces/__init__.py +5 -0
  42. nexsandglass/interfaces/nexsandglass.py +123 -0
  43. nexsandglass/interfaces/nyx.py +444 -0
  44. nexsandglass/interfaces/plugin.py +59 -0
  45. nexsandglass/interfaces/sandglass_mcp.py +391 -0
  46. nexsandglass/l3/__init__.py +113 -0
  47. nexsandglass/l3/emotion_l3.py +429 -0
  48. nexsandglass/l3/l3_persona.py +124 -0
  49. nexsandglass/l3/l3_persona_verify.py +143 -0
  50. nexsandglass/l3/l3_search_core.py +513 -0
  51. nexsandglass/l3/l3_tasks.py +93 -0
  52. nexsandglass/l3/offset_l3.py +522 -0
  53. nexsandglass/l3/offset_signals.py +34 -0
  54. nexsandglass/l3/persona_l3.py +626 -0
  55. nexsandglass/l3/scene_l3.py +407 -0
  56. nexsandglass/l3/weave_l3.py +416 -0
  57. nexsandglass/migrate_to_chromadb.py +60 -0
  58. nexsandglass/nyx_server.py +357 -0
  59. nexsandglass/runtime/__init__.py +38 -0
  60. nexsandglass/runtime/bundle.py +324 -0
  61. nexsandglass/runtime/consolidation.py +270 -0
  62. nexsandglass/runtime/eval.py +126 -0
  63. nexsandglass/runtime/facade.py +357 -0
  64. nexsandglass/runtime/intent.py +295 -0
  65. nexsandglass/runtime/orchestrator.py +779 -0
  66. nexsandglass/runtime/promotion.py +409 -0
  67. nexsandglass/utils/__init__.py +19 -0
  68. nexsandglass/utils/discipline.py +66 -0
  69. nexsandglass/utils/heartbeat.py +104 -0
  70. nyx_memory-3.4.2.dist-info/METADATA +444 -0
  71. nyx_memory-3.4.2.dist-info/RECORD +74 -0
  72. nyx_memory-3.4.2.dist-info/WHEEL +5 -0
  73. nyx_memory-3.4.2.dist-info/licenses/LICENSE +21 -0
  74. nyx_memory-3.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ """NexSandglass — 记忆感知系统
2
+
3
+ 夜神 Nyx:跨会话记忆、知识图谱、模糊感知(Déjà Vu)
4
+ """
5
+
6
+ from .core.sandglass_paths import __version__
7
+ from .utils import health_summary, tick
8
+
9
+ from .core import (
10
+ NexSandglassProvider,
11
+ SearchRouter,
12
+ detect,
13
+ log_message,
14
+ register_provider,
15
+ validate,
16
+ )
17
+ from .l3 import (
18
+ entropy_ghost,
19
+ glass_reminder,
20
+ offset_check,
21
+ persona_build,
22
+ persona_project,
23
+ persona_update,
24
+ simhash,
25
+ stage_list,
26
+ task_pending,
27
+ weave_contradiction,
28
+ weave_graph,
29
+ )
30
+ from .features import (
31
+ count,
32
+ comprehensive_offset,
33
+ distill,
34
+ entropy_chart,
35
+ export_soul,
36
+ full_sanity,
37
+ log,
38
+ memory_migrate,
39
+ merge_soul,
40
+ night_watch,
41
+ pulse,
42
+ ratio,
43
+ recent,
44
+ search,
45
+ search_semantic,
46
+ session_context,
47
+ shadow_search,
48
+ timeline,
49
+ wthread_add,
50
+ wthread_extract,
51
+ wthread_graph,
52
+ wthread_query,
53
+ wthread_stats,
54
+ wthread_store,
55
+ wthread_weave,
56
+ )
57
+
58
+ __author__ = "sixgod"
59
+
60
+ __all__ = [
61
+ # core
62
+ "NexSandglassProvider",
63
+ "register_provider",
64
+ "SearchRouter",
65
+ "log_message",
66
+ "validate",
67
+ # storage
68
+ "search",
69
+ "recent",
70
+ "count",
71
+ "timeline",
72
+ "search_semantic",
73
+ "shadow_search",
74
+ # thread graph
75
+ "wthread_extract",
76
+ "wthread_store",
77
+ "wthread_query",
78
+ "wthread_stats",
79
+ "wthread_weave",
80
+ "wthread_add",
81
+ "wthread_graph",
82
+ # persona / offset / emotion
83
+ "comprehensive_offset",
84
+ "offset_check",
85
+ "persona_build",
86
+ "persona_update",
87
+ "persona_project",
88
+ "stage_list",
89
+ "glass_reminder",
90
+ "entropy_ghost",
91
+ "weave_contradiction",
92
+ "weave_graph",
93
+ "simhash",
94
+ # task / think
95
+ "task_pending",
96
+ "distill",
97
+ "session_context",
98
+ "entropy_chart",
99
+ "full_sanity",
100
+ # decision particles
101
+ "log",
102
+ "ratio",
103
+ "detect",
104
+ # migrate / soul
105
+ "memory_migrate",
106
+ "export_soul",
107
+ "merge_soul",
108
+ # runtime
109
+ "pulse",
110
+ "night_watch",
111
+ "tick",
112
+ "health_summary",
113
+ ]
@@ -0,0 +1,67 @@
1
+ """Core infrastructure: memory backend, search routing, storage."""
2
+
3
+ from .emotion_vocab import detect, learn, load_vocab, mood_message, save_vocab
4
+ from .l0_buffer import l0_context, l0_remember, l0_size
5
+ from .memory_provider import NexSandglassProvider, register as register_provider
6
+ from .sandglass import count, read
7
+ from .sandglass_archive import (
8
+ archive_path,
9
+ archive_stats,
10
+ cold_migration,
11
+ is_old,
12
+ parse_ts,
13
+ search_archive,
14
+ )
15
+ from .sandglass_log import log_conversation, log_message
16
+ from .sandglass_paths import validate
17
+ from .sandglass_sqlite import search, search_in, search_year, sync_all, sync_incremental
18
+ from .search_router import (
19
+ Fts5Search,
20
+ IdxSearch,
21
+ MmapFallback,
22
+ SearchRouter,
23
+ ShadowSearch,
24
+ TfidfSearch,
25
+ )
26
+
27
+ __all__ = [
28
+ # memory provider (Hermes plugin)
29
+ "NexSandglassProvider",
30
+ "register_provider",
31
+ # search routing
32
+ "SearchRouter",
33
+ "ShadowSearch",
34
+ "Fts5Search",
35
+ "IdxSearch",
36
+ "TfidfSearch",
37
+ "MmapFallback",
38
+ # sqlite FTS layer
39
+ "search",
40
+ "search_in",
41
+ "search_year",
42
+ "sync_all",
43
+ "sync_incremental",
44
+ # sandglass log / archive
45
+ "log_message",
46
+ "log_conversation",
47
+ "archive_path",
48
+ "archive_stats",
49
+ "cold_migration",
50
+ "is_old",
51
+ "parse_ts",
52
+ "search_archive",
53
+ # emotion vocab
54
+ "load_vocab",
55
+ "save_vocab",
56
+ "learn",
57
+ "detect",
58
+ "mood_message",
59
+ # paths / flat sandglass reader
60
+ "validate",
61
+ "read",
62
+ "count",
63
+ # L0 buffer
64
+ "l0_remember",
65
+ "l0_context",
66
+ "l0_size",
67
+ ]
@@ -0,0 +1,176 @@
1
+ """
2
+ core/embedding_provider.py — 可插拔 Embedding 后端
3
+
4
+ 设计:
5
+ - 抽象 EmbeddingProvider 接口,支持本地模型(sentence-transformers)和外部 API
6
+ - 默认走本地免费方案(不强制联网/付费)
7
+ - 懒加载:首次调用时才初始化模型,避免 import 时开销
8
+ - Fail-safe:模型加载失败时返回 None(调用方退回词法检索)
9
+
10
+ 本地默认模型:paraphrase-multilingual-MiniLM-L12-v2
11
+ - 支持中英混合(50+ 语言)
12
+ - 向量维度 384(轻量)
13
+ - ~50MB 磁盘,首次下载后离线可用
14
+
15
+ 安装依赖(可选):
16
+ pip install sentence-transformers numpy
17
+ 未安装时自动降级为 None(不影响现有词法检索)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import os
24
+ from typing import Optional
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # 向量维度常量(与默认模型一致)
29
+ EMBEDDING_DIM: int = 384
30
+
31
+
32
+ class EmbeddingProvider:
33
+ """Embedding 后端抽象接口。"""
34
+
35
+ def encode(self, texts: list[str]) -> list[list[float]]:
36
+ """将文本列表编码为向量列表。失败返回空列表。"""
37
+ raise NotImplementedError
38
+
39
+ def encode_one(self, text: str) -> Optional[list[float]]:
40
+ """单条编码。"""
41
+ results = self.encode([text])
42
+ return results[0] if results else None
43
+
44
+ @property
45
+ def dim(self) -> int:
46
+ return EMBEDDING_DIM
47
+
48
+ @property
49
+ def available(self) -> bool:
50
+ return True
51
+
52
+
53
+ class SentenceTransformerProvider(EmbeddingProvider):
54
+ """基于 sentence-transformers 的本地 Embedding(中英混合支持)。"""
55
+
56
+ def __init__(self, model_name: str = "paraphrase-multilingual-MiniLM-L12-v2"):
57
+ self._model_name = model_name
58
+ self._model = None
59
+
60
+ def _ensure_model(self):
61
+ if self._model is not None:
62
+ return
63
+ try:
64
+ from sentence_transformers import SentenceTransformer
65
+ # 本地缓存目录(离线可用)
66
+ cache_dir = os.path.expanduser("~/.cache/nexsandglass/models")
67
+ os.makedirs(cache_dir, exist_ok=True)
68
+ self._model = SentenceTransformer(self._model_name, cache_folder=cache_dir)
69
+ logger.info("[EmbeddingProvider] 本地模型加载成功: %s", self._model_name)
70
+ except Exception as e:
71
+ logger.warning("[EmbeddingProvider] 本地模型加载失败(将退回词法检索): %s", e)
72
+ self._model = None
73
+
74
+ def encode(self, texts: list[str]) -> list[list[float]]:
75
+ if not texts:
76
+ return []
77
+ self._ensure_model()
78
+ if self._model is None:
79
+ return []
80
+ try:
81
+ import numpy as np
82
+ embeddings = self._model.encode(texts, normalize_embeddings=True, show_progress_bar=False)
83
+ return embeddings.tolist()
84
+ except Exception as e:
85
+ logger.warning("[EmbeddingProvider] 编码失败: %s", e)
86
+ return []
87
+
88
+ @property
89
+ def available(self) -> bool:
90
+ self._ensure_model()
91
+ return self._model is not None
92
+
93
+
94
+ class ExternalAPIProvider(EmbeddingProvider):
95
+ """外部 Embedding API(OpenAI 兼容接口,可选)。"""
96
+
97
+ def __init__(self, api_url: str = "", api_key: str = "", model: str = "text-embedding-3-small"):
98
+ self._api_url = api_url or os.environ.get("EMBEDDING_API_URL", "")
99
+ self._api_key = api_key or os.environ.get("EMBEDDING_API_KEY", "")
100
+ self._model = model
101
+ self._dim = EMBEDDING_DIM if "3-small" in model else 1536
102
+
103
+ def encode(self, texts: list[str]) -> list[list[float]]:
104
+ if not texts or not self._api_url:
105
+ return []
106
+ try:
107
+ import httpx
108
+ resp = httpx.post(
109
+ self._api_url,
110
+ headers={"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"},
111
+ json={"input": texts, "model": self._model},
112
+ timeout=30,
113
+ )
114
+ resp.raise_for_status()
115
+ data = resp.json()
116
+ return [item["embedding"] for item in data.get("data", [])]
117
+ except Exception as e:
118
+ logger.warning("[ExternalAPIProvider] 调用失败: %s", e)
119
+ return []
120
+
121
+ @property
122
+ def dim(self) -> int:
123
+ return self._dim
124
+
125
+ @property
126
+ def available(self) -> bool:
127
+ return bool(self._api_url)
128
+
129
+
130
+ class NullProvider(EmbeddingProvider):
131
+ """不可用时的空实现(fail-safe)。"""
132
+
133
+ def encode(self, texts: list[str]) -> list[list[float]]:
134
+ return []
135
+
136
+ def encode_one(self, text: str) -> Optional[list[float]]:
137
+ return None
138
+
139
+ @property
140
+ def dim(self) -> int:
141
+ return EMBEDDING_DIM
142
+
143
+ @property
144
+ def available(self) -> bool:
145
+ return False
146
+
147
+
148
+ # ── 全局单例(懒加载)──────────────────────────────────────────
149
+
150
+ _provider: Optional[EmbeddingProvider] = None
151
+
152
+
153
+ def get_embedding_provider() -> EmbeddingProvider:
154
+ """获取全局 EmbeddingProvider 单例(优先本地,可选外部 API)。"""
155
+ global _provider
156
+ if _provider is not None:
157
+ return _provider
158
+ # 检查是否配置外部 API
159
+ api_url = os.environ.get("EMBEDDING_API_URL", "")
160
+ if api_url:
161
+ _provider = ExternalAPIProvider(api_url=api_url)
162
+ if _provider.available:
163
+ return _provider
164
+ # 默认本地
165
+ local = SentenceTransformerProvider()
166
+ if local.available:
167
+ _provider = local
168
+ else:
169
+ _provider = NullProvider()
170
+ return _provider
171
+
172
+
173
+ def reset_provider():
174
+ """测试用:重置单例。"""
175
+ global _provider
176
+ _provider = None
@@ -0,0 +1,186 @@
1
+ """
2
+ NexSandglass 情绪词库 — 七大情绪分类
3
+ =====================================
4
+ 基于 Ekman + Plutchik 情绪理论。
5
+ 动态学习 + 主语判断 + 小二回应策略。
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import re
11
+
12
+ from nexsandglass.core.sandglass_paths import _NB
13
+ _VOCAB_FILE = os.path.join(_NB, "emotion_vocab.json")
14
+
15
+ # ── 七大情绪分类 ──
16
+ # 格式:{大类: {子类: [词], 管家策略: "xxx"}}
17
+ _BUILTIN = {
18
+ "愤怒": {
19
+ "zh": ["气死", "恼火", "凭什么", "不公平", "过分", "太气人了", "火大", "无语", "受不了", "真恶心", "怎么这样", "太糟糕了", "烦死了", "气死我了", "烦死", "气炸", "真烦", "真火大", "要气死", "被气到", "好生气", "受够了", "崩溃了", "无语了", "恶心", "恼人", "操蛋", "扯淡", "折磨", "劝退", "坑爹", "垃圾", "烂", "废了", "有毒"],
20
+ "en": ["angry", "pissed off", "outrageous", "unfair", "ridiculous", "unacceptable"],
21
+ "策略": "缓提醒。先让情绪降下来,不提待办。",
22
+ "优先级": "高",
23
+ },
24
+ "悲伤": {
25
+ "zh": ["难过", "伤心", "好累", "不想动", "心累", "崩溃", "失望", "没劲", "没意思", "提不起劲", "好难过", "废物", "没用", "搞砸了", "又错了", "做不好", "失败", "不行了", "完蛋了", "好难", "太难了", "做不动", "撑不住", "痛苦"],
26
+ "en": ["sad", "depressed", "heartbroken", "disappointed", "exhausted", "drained",
27
+ "terrible", "failure", "useless", "messed up", "can't do this"],
28
+ "策略": "缓提醒。状态不好的时候不催。",
29
+ "优先级": "高",
30
+ },
31
+ "焦虑": {
32
+ "zh": ["焦虑", "紧张", "压力好大", "害怕", "不安", "担心",
33
+ "怎么办", "万一", "会不会", "好怕"],
34
+ "en": ["anxious", "nervous", "stressed", "worried", "afraid", "scared"],
35
+ "策略": "不催。给安全感,不提额外负担。",
36
+ "优先级": "中",
37
+ },
38
+ "放弃": {
39
+ "zh": ["不管了", "随便", "放弃", "累了", "不想做了", "就这样吧", "不做了", "算了算了",
40
+ "不纠结了", "爱咋咋地", "无所谓", "能用就行"],
41
+ "en": ["whatever", "give up", "i don't care", "fine, do what you want", "i quit"],
42
+ "策略": "红牌。提醒全停,优先级=自我修正。",
43
+ "优先级": "最高",
44
+ },
45
+ "开心": {
46
+ "zh": ["开心", "太好了", "太棒了", "满意", "有意思", "值得", "兴奋", "好棒", "哈哈", "nice", "真不错", "有成就感", "爽", "喜欢", "终于", "很好", "不错", "舒服", "完美", "真香", "赞", "厉害", "搞定", "跑通了", "稳了", "通了", "漂亮", "优秀", "强大", "惊喜", "感动", "成就感", "自豪", "高兴", "愉快", "轻松", "好使", "管用", "真快", "丝滑", "起飞", "牛批", "无敌", "爱了", "推荐", "好用", "简洁", "清晰", "优雅"],
47
+ "en": ["happy", "great", "excited", "awesome", "love it", "worth it", "amazing"],
48
+ "策略": "正常提醒。状态好可以多提醒。",
49
+ "优先级": "低",
50
+ },
51
+ "困惑": {
52
+ "zh": ["不懂", "不明白", "怎么回事", "啥意思", "搞不懂", "奇怪",
53
+ "不对劲", "不太对", "有点奇怪", "没搞明白"],
54
+ "en": ["confused", "don't understand", "what's going on", "doesn't make sense"],
55
+ "策略": "正常。帮助澄清,不影响提醒。",
56
+ "优先级": "低",
57
+ },
58
+ "意外": {
59
+ "zh": ["没想到", "竟然", "不是吧", "真的假的", "天哪",
60
+ "怎么回事这样", "原来是"],
61
+ "en": ["wow", "unexpected", "surprised", "really", "no way", "oh my"],
62
+ "策略": "正常。分享惊喜,不影响提醒。",
63
+ "优先级": "低",
64
+ },
65
+ }
66
+
67
+ # ── 主语标记 ──
68
+ _SUBJECT_OTHERS = ["他", "她", "他们", "那个人", "别人",
69
+ "he ", "she ", "they ", "that person", "someone"]
70
+ _SUBJECT_IMPACT = ["他让", "她让", "他们让", "害得",
71
+ "he makes", "she makes", "they make"]
72
+
73
+
74
+ def load_vocab() -> dict:
75
+ """加载情绪词库。合并内置词库确保不丢失。"""
76
+ if os.path.exists(_VOCAB_FILE):
77
+ try:
78
+ with open(_VOCAB_FILE, "r", encoding="utf-8") as f:
79
+ vocab = json.loads(f.read())
80
+ for mood, data in _BUILTIN.items():
81
+ if mood not in vocab:
82
+ vocab[mood] = {}
83
+ for key in ["zh", "en", "策略", "优先级"]:
84
+ if key not in vocab[mood]:
85
+ vocab[mood][key] = data.get(key, [])
86
+ elif isinstance(data[key], list):
87
+ existing = set(vocab[mood][key])
88
+ existing.update(data[key])
89
+ vocab[mood][key] = sorted(existing)
90
+ return vocab
91
+ except Exception:
92
+ pass
93
+
94
+ vocab = {mood: {k: v for k, v in data.items()} for mood, data in _BUILTIN.items()}
95
+ save_vocab(vocab)
96
+ return vocab
97
+
98
+
99
+ def save_vocab(vocab: dict):
100
+ os.makedirs(os.path.dirname(_VOCAB_FILE), exist_ok=True)
101
+ with open(_VOCAB_FILE, "w", encoding="utf-8") as f:
102
+ json.dump(vocab, f, ensure_ascii=False, indent=2)
103
+
104
+
105
+ def learn(word: str, mood: str, lang: str = "zh") -> bool:
106
+ """学习新情绪词。"""
107
+ vocab = load_vocab()
108
+ if mood not in vocab:
109
+ return False
110
+ if lang not in vocab[mood]:
111
+ vocab[mood][lang] = []
112
+ if word not in vocab[mood][lang]:
113
+ vocab[mood][lang].append(word)
114
+ vocab[mood][lang] = sorted(vocab[mood][lang])
115
+ save_vocab(vocab)
116
+ return True
117
+ return False
118
+
119
+
120
+ def detect(message: str) -> dict:
121
+ """检测情绪:返回 {mood, emitter, keywords, strategy, priority}。"""
122
+ # 静默模式
123
+ if os.environ.get("NX_MODE") == "neutral":
124
+ return {"mood": "", "emitter": "自我", "keywords": [], "strategy": "", "priority": "低"}
125
+ # ── 否定词列表 ──
126
+ _NEGATION = ["不", "没", "不是", "不太", "并不", "并不太",
127
+ "not ", "don't ", "doesn't ", "isn't ", "aren't ",
128
+ "never ", "no ", "hardly "]
129
+
130
+ vocab = load_vocab()
131
+ msg_lower = message.lower()
132
+
133
+ mood_order = ["放弃", "愤怒", "悲伤", "焦虑", "困惑", "意外", "开心"]
134
+
135
+ for mood in mood_order:
136
+ all_words = vocab.get(mood, {}).get("zh", []) + vocab.get(mood, {}).get("en", [])
137
+ for word in sorted(all_words, key=len, reverse=True):
138
+ idx = msg_lower.find(word.lower())
139
+ if idx >= 0:
140
+ # ── 否定检查:开心词前面有否定 → 跳过 ──
141
+ if mood in ("开心", "积极"):
142
+ ctx8 = msg_lower[max(0, idx-8):idx]
143
+ if any(n in ctx8 for n in _NEGATION):
144
+ continue
145
+
146
+ ctx = message[max(0, idx-30):idx].lower()
147
+ if any(w in ctx for w in _SUBJECT_OTHERS):
148
+ emitter = "他人"
149
+ elif any(w in ctx for w in _SUBJECT_IMPACT):
150
+ emitter = "影响"
151
+ else:
152
+ emitter = "自我"
153
+
154
+ return {
155
+ "mood": mood, "emitter": emitter, "keywords": [word],
156
+ "strategy": vocab[mood].get("策略", ""),
157
+ "priority": vocab[mood].get("优先级", "低"),
158
+ }
159
+
160
+ return {"mood": "", "emitter": "自我", "keywords": [], "strategy": "", "priority": "低"}
161
+
162
+
163
+ def mood_message(detection: dict) -> str:
164
+ """管家回应。"""
165
+ mood = detection["mood"]
166
+ emitter = detection["emitter"]
167
+ words = detection["keywords"][:2]
168
+ strat = detection.get("strategy", "")
169
+
170
+ if not mood:
171
+ return ""
172
+
173
+ emoji_map = {
174
+ "愤怒": "😡", "悲伤": "😢", "焦虑": "😰", "放弃": "🔴",
175
+ "开心": "😊", "困惑": "🤔", "意外": "😲",
176
+ }
177
+ emoji = emoji_map.get(mood, "🟡")
178
+
179
+ if emitter == "他人":
180
+ return f"{emoji} 觉察:别人{mood}——「{'、'.join(words)}」。不影响你的状态。"
181
+ elif emitter == "影响":
182
+ return f"{emoji} 觉察:别人的情绪影响到你了——「{'、'.join(words)}」。{strat}"
183
+ elif mood == "放弃":
184
+ return f"🔴 红牌——「{'、'.join(words)}」。优先级=自我修正。{strat}"
185
+ else:
186
+ return f"{emoji} 觉察:{mood}——「{'、'.join(words)}」。{strat}"
@@ -0,0 +1,75 @@
1
+ """
2
+ NexSandglass — L0 短期记忆缓冲区
3
+ =================================
4
+ 记录最近5轮对话,不进入倒排索引(不污染搜索)。
5
+ 满5轮自动蒸馏最新一轮到L1。
6
+
7
+ 用法:
8
+ from nexsandglass.core.l0_buffer import l0_remember, l0_context
9
+ l0_remember("用户说了一句重要的话")
10
+ context = l0_context() # 返回最近5轮的上下文
11
+ """
12
+ import os
13
+ import json
14
+ from datetime import datetime
15
+ from nexsandglass.core.sandglass_paths import _NB
16
+
17
+ L0_PATH = os.path.join(_NB, "l0_buffer.jsonl")
18
+ L0_MAX = 5 # 最多保留5轮
19
+
20
+
21
+ def l0_remember(text: str, speaker: str = "user") -> None:
22
+ """记入L0缓冲区。满L0_MAX时蒸馏最旧一条到L1。"""
23
+ entry = {"ts": datetime.now().isoformat(), "speaker": speaker, "text": text[:500]}
24
+ os.makedirs(os.path.dirname(L0_PATH), exist_ok=True)
25
+
26
+ with open(L0_PATH, "a", encoding="utf-8") as f:
27
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
28
+
29
+ # 检查是否满了——满则蒸馏最旧一条到L1
30
+ with open(L0_PATH, "r", encoding="utf-8") as f:
31
+ lines = f.readlines()
32
+
33
+ if len(lines) > L0_MAX:
34
+ # 蒸馏最旧一条
35
+ oldest = json.loads(lines[0].strip())
36
+ try:
37
+ from nexsandglass.core.sandglass_log import log_message
38
+ log_message(f"[L0-auto] {oldest['speaker']}: {oldest['text'][:300]}", oldest.get("speaker", "user"))
39
+ from nexsandglass.features.decision_particles import log as dp_log
40
+ dp_log(oldest["text"][:100], f"L0_buffer_distill")
41
+ # 🆕 蒸馏即偏移检测——用全文,不只是前100字
42
+ from nexsandglass.l3.offset_l3 import offset_check
43
+ offset_check(oldest["text"][:300], user_persisted=False)
44
+ except Exception:
45
+ pass
46
+
47
+ # 只保留最近 L0_MAX 条
48
+ with open(L0_PATH, "w", encoding="utf-8") as f:
49
+ f.writelines(lines[-L0_MAX:])
50
+
51
+
52
+ def l0_context() -> str:
53
+ """返回L0缓冲区的上下文(最近5轮)。"""
54
+ if not os.path.exists(L0_PATH):
55
+ return ""
56
+ with open(L0_PATH, "r", encoding="utf-8") as f:
57
+ lines = f.readlines()[-L0_MAX:]
58
+ if not lines:
59
+ return ""
60
+ parts = []
61
+ for line in lines:
62
+ try:
63
+ e = json.loads(line.strip())
64
+ parts.append(f"[{e.get('speaker','?')}] {e.get('text','')[:200]}")
65
+ except Exception:
66
+ pass
67
+ return "## L0 短期记忆\n" + "\n".join(parts)
68
+
69
+
70
+ def l0_size() -> int:
71
+ """L0当前条数。"""
72
+ if not os.path.exists(L0_PATH):
73
+ return 0
74
+ with open(L0_PATH, "r", encoding="utf-8") as f:
75
+ return sum(1 for _ in f)