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.
- nexsandglass/__init__.py +113 -0
- nexsandglass/core/__init__.py +67 -0
- nexsandglass/core/embedding_provider.py +176 -0
- nexsandglass/core/emotion_vocab.py +186 -0
- nexsandglass/core/l0_buffer.py +75 -0
- nexsandglass/core/llm_extract.py +81 -0
- nexsandglass/core/memory_provider.py +591 -0
- nexsandglass/core/sandglass.py +75 -0
- nexsandglass/core/sandglass_archive.py +149 -0
- nexsandglass/core/sandglass_log.py +124 -0
- nexsandglass/core/sandglass_paths.py +41 -0
- nexsandglass/core/sandglass_sqlite.py +163 -0
- nexsandglass/core/search_router.py +304 -0
- nexsandglass/core/vector_search.py +94 -0
- nexsandglass/core/vector_store.py +227 -0
- nexsandglass/engram/__init__.py +126 -0
- nexsandglass/engram/bridge.py +272 -0
- nexsandglass/engram/context.py +194 -0
- nexsandglass/engram/decay.py +278 -0
- nexsandglass/engram/dream_pipeline.py +242 -0
- nexsandglass/engram/evolve.py +143 -0
- nexsandglass/engram/loops/__init__.py +67 -0
- nexsandglass/engram/loops/dream_engram.py +160 -0
- nexsandglass/engram/loops/fact_thread.py +195 -0
- nexsandglass/engram/loops/persona_ctx.py +137 -0
- nexsandglass/engram/loops/recall_writer.py +173 -0
- nexsandglass/engram/loops/temporal_fact.py +272 -0
- nexsandglass/engram/types.py +295 -0
- nexsandglass/engram/writer.py +240 -0
- nexsandglass/features/__init__.py +112 -0
- nexsandglass/features/decision_particles.py +715 -0
- nexsandglass/features/multi_analysis.py +264 -0
- nexsandglass/features/nightwatch.py +151 -0
- nexsandglass/features/pulse.py +328 -0
- nexsandglass/features/sandglass_chroma.py +103 -0
- nexsandglass/features/sandglass_think.py +1505 -0
- nexsandglass/features/sandglass_vault.py +561 -0
- nexsandglass/features/shadow_sand.py +243 -0
- nexsandglass/features/soul_diff.py +112 -0
- nexsandglass/features/weavethread.py +328 -0
- nexsandglass/interfaces/__init__.py +5 -0
- nexsandglass/interfaces/nexsandglass.py +123 -0
- nexsandglass/interfaces/nyx.py +444 -0
- nexsandglass/interfaces/plugin.py +59 -0
- nexsandglass/interfaces/sandglass_mcp.py +391 -0
- nexsandglass/l3/__init__.py +113 -0
- nexsandglass/l3/emotion_l3.py +429 -0
- nexsandglass/l3/l3_persona.py +124 -0
- nexsandglass/l3/l3_persona_verify.py +143 -0
- nexsandglass/l3/l3_search_core.py +513 -0
- nexsandglass/l3/l3_tasks.py +93 -0
- nexsandglass/l3/offset_l3.py +522 -0
- nexsandglass/l3/offset_signals.py +34 -0
- nexsandglass/l3/persona_l3.py +626 -0
- nexsandglass/l3/scene_l3.py +407 -0
- nexsandglass/l3/weave_l3.py +416 -0
- nexsandglass/migrate_to_chromadb.py +60 -0
- nexsandglass/nyx_server.py +357 -0
- nexsandglass/runtime/__init__.py +38 -0
- nexsandglass/runtime/bundle.py +324 -0
- nexsandglass/runtime/consolidation.py +270 -0
- nexsandglass/runtime/eval.py +126 -0
- nexsandglass/runtime/facade.py +357 -0
- nexsandglass/runtime/intent.py +295 -0
- nexsandglass/runtime/orchestrator.py +779 -0
- nexsandglass/runtime/promotion.py +409 -0
- nexsandglass/utils/__init__.py +19 -0
- nexsandglass/utils/discipline.py +66 -0
- nexsandglass/utils/heartbeat.py +104 -0
- nyx_memory-3.4.2.dist-info/METADATA +444 -0
- nyx_memory-3.4.2.dist-info/RECORD +74 -0
- nyx_memory-3.4.2.dist-info/WHEEL +5 -0
- nyx_memory-3.4.2.dist-info/licenses/LICENSE +21 -0
- nyx_memory-3.4.2.dist-info/top_level.txt +1 -0
nexsandglass/__init__.py
ADDED
|
@@ -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)
|