dsh-biocore 0.3.3 → 0.5.0

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 (59) hide show
  1. package/lib/index.js +30 -1
  2. package/lib/modules/standby.js +61 -0
  3. package/lib/modules/state.js +22 -3
  4. package/package.json +1 -1
  5. package/python/biocore_memory/core/config.py +7 -3
  6. package/python/biocore_memory/engine.py +91 -6
  7. package/python/biocore_memory/recall/embedding.py +0 -51
  8. package/python/biocore_memory/recall/feedback.py +34 -7
  9. package/python/biocore_memory/recall/fusion.py +5 -1
  10. package/python/biocore_memory/scoring.py +203 -0
  11. package/python/biocore_memory/space.py +355 -0
  12. package/python/biocore_memory/store/dedupe.py +1 -0
  13. package/python/biocore_memory/store/tiering.py +14 -3
  14. package/python/biocore_model/affective_labor.py +14 -1
  15. package/python/biocore_model/appraisal.py +172 -3
  16. package/python/biocore_model/bio_tick.py +43 -13
  17. package/python/biocore_model/emotion_geometry.py +9 -0
  18. package/python/biocore_model/envy.py +21 -0
  19. package/python/biocore_model/feedback_memory.py +53 -0
  20. package/python/biocore_model/needs_events.py +32 -11
  21. package/python/biocore_model/regulation_strategies.py +5 -0
  22. package/python/biocore_model/repair_after_boundary.py +1 -0
  23. package/python/biocore_model/self_esteem.py +14 -2
  24. package/python/biocore_model/self_intent.py +6 -4
  25. package/python/biocore_model/tests/test_autonomy_self.py +153 -153
  26. package/python/biocore_model/tests/test_cache_series.py +137 -0
  27. package/python/biocore_model/tests/test_core_state.py +240 -240
  28. package/python/biocore_model/tests/test_extensions.py +167 -167
  29. package/python/biocore_model/tests/test_field_arbiter.py +444 -0
  30. package/python/biocore_model/tests/test_governance.py +181 -181
  31. package/python/biocore_model/tests/test_lifecycle.py +153 -0
  32. package/python/biocore_model/tests/test_memory_inject.py +211 -211
  33. package/python/biocore_model/tests/test_retrieval_plus.py +143 -0
  34. package/python/biocore_model/tests/test_signals.py +106 -106
  35. package/python/biocore_model/tests/test_standby.py +199 -0
  36. package/python/biocore_runtime/__init__.py +3 -0
  37. package/python/biocore_runtime/arbiter.py +307 -0
  38. package/python/biocore_runtime/batch_b.py +724 -0
  39. package/python/biocore_runtime/batch_c.py +186 -0
  40. package/python/biocore_runtime/batch_d.py +659 -0
  41. package/python/biocore_runtime/batch_e.py +328 -0
  42. package/python/biocore_runtime/batch_registry.py +446 -0
  43. package/python/biocore_runtime/cache_stats.py +161 -0
  44. package/python/biocore_runtime/core_state.py +106 -3
  45. package/python/biocore_runtime/dimensions.py +419 -0
  46. package/python/biocore_runtime/expression.py +261 -0
  47. package/python/biocore_runtime/field.py +440 -0
  48. package/python/biocore_runtime/lifecycle.py +233 -0
  49. package/python/biocore_runtime/perception.py +1045 -0
  50. package/python/biocore_runtime/pipeline.py +4458 -2974
  51. package/python/biocore_runtime/priority.py +210 -0
  52. package/python/biocore_runtime/retrieval_plus.py +352 -0
  53. package/python/biocore_runtime/standby.py +581 -472
  54. package/python/biocore_runtime/timescales.py +263 -0
  55. package/python/data/signals.json +3266 -206
  56. package/ui/panel-layout.js +32 -29
  57. package/ui/panel.css +563 -974
  58. package/ui/panel.html +427 -401
  59. package/ui/panel.js +543 -450
@@ -1,211 +1,211 @@
1
- # -*- coding: utf-8 -*-
2
- """task-221 (batch 2): query recall injection + embedding stats persistence."""
3
- import json
4
- import os
5
- import tempfile
6
- import unittest
7
- from pathlib import Path
8
-
9
- from biocore_memory.recall import embedding
10
- from biocore_runtime import pipeline
11
-
12
-
13
- def entry(i, text, ts, importance=0.5):
14
- return {"id": i, "text": text, "role": "user", "ts": ts,
15
- "importance": importance, "hits": 1, "scope": "default",
16
- "memory_confidence": 0.7, "memory_source": "direct"}
17
-
18
-
19
- class InjectCase(unittest.TestCase):
20
- def setUp(self):
21
- self.tmp = tempfile.TemporaryDirectory()
22
- self.root = Path(self.tmp.name)
23
- self.store = self.root / "data" / "memory"
24
- self.entries = [
25
- entry("new", "今天买了咖啡", 1700000300.0),
26
- entry("old", "上次说的苹果很甜", 1700000100.0),
27
- entry("mid", "明天要去公园", 1700000200.0),
28
- ]
29
- (self.store / "sessions" / "s1").mkdir(parents=True)
30
- (self.store / "sessions" / "s1" / "memory.json").write_text(
31
- json.dumps({"entries": self.entries}), encoding="utf-8")
32
-
33
- def tearDown(self):
34
- self.tmp.cleanup()
35
-
36
- def inject(self, **kwargs):
37
- return pipeline.inject_block("s1", str(self.store), **kwargs)
38
-
39
-
40
- class QueryModeTest(InjectCase):
41
- def test_query_recall_orders_by_query(self):
42
- out = self.inject(query="苹果", mode="query", top_k=3)
43
- self.assertEqual(out["ids"][0], "old")
44
-
45
- def test_recent_mode_keeps_newest_first(self):
46
- out = self.inject(query="苹果", mode="recent", top_k=3)
47
- self.assertEqual(out["ids"][0], "new")
48
-
49
- def test_default_mode_is_query(self):
50
- out = self.inject(query="苹果", top_k=2)
51
- self.assertEqual(out["ids"][0], "old")
52
- self.assertEqual(len(out["ids"]), 2)
53
-
54
- def test_empty_query_falls_back_recent(self):
55
- out = self.inject(query="", mode="query", top_k=3)
56
- self.assertEqual(out["ids"][0], "new")
57
-
58
- def test_engine_failure_falls_back_recent(self):
59
- saved = pipeline._inject_engine
60
- pipeline._inject_engine = lambda store, entries: (_ for _ in ()).throw(RuntimeError("boom"))
61
- try:
62
- out = self.inject(query="苹果", mode="query", top_k=2)
63
- finally:
64
- pipeline._inject_engine = saved
65
- self.assertEqual(out["ids"][0], "new")
66
- self.assertEqual(len(out["ids"]), 2)
67
-
68
- def test_output_format_and_budget(self):
69
- out = self.inject(query="苹果", mode="query", top_k=3, max_chars=60)
70
- self.assertTrue(out["text"].startswith("你记得这些事:"))
71
- self.assertTrue(all(line.startswith("- ") for line in out["text"].split("\n")[1:]))
72
- self.assertLessEqual(out["chars"], 60)
73
- self.assertEqual(out["chars"], len(out["text"]))
74
- self.assertTrue(set(out["ids"]).issubset({"new", "old", "mid"}))
75
-
76
-
77
- class RerankTest(InjectCase):
78
- def test_rerank_reorders_when_enabled(self):
79
- class FakeRerank:
80
- def __init__(self):
81
- self.calls = 0
82
-
83
- def rerank_documents(self, query, documents, config):
84
- self.calls += 1
85
- return [{"index": 2}, {"index": 0}, {"index": 1}]
86
-
87
- fake = FakeRerank()
88
- saved = pipeline._rerank_mod
89
- pipeline._rerank_mod = fake
90
- try:
91
- out = self.inject(query="苹果", mode="query", top_k=8,
92
- rerank_config={"rerank_enabled": True})
93
- finally:
94
- pipeline._rerank_mod = saved
95
- self.assertEqual(fake.calls, 1)
96
- # engine put 苹果 (old) first; rerank's order must win instead
97
- self.assertEqual(out["ids"], ["mid", "old", "new"])
98
-
99
- def test_rerank_off_does_not_call(self):
100
- class Exploding:
101
- def rerank_documents(self, *args, **kwargs):
102
- raise AssertionError("rerank must not run")
103
-
104
- saved = pipeline._rerank_mod
105
- pipeline._rerank_mod = Exploding()
106
- try:
107
- out = self.inject(query="苹果", mode="query", top_k=8,
108
- rerank_config={"rerank_enabled": False})
109
- finally:
110
- pipeline._rerank_mod = saved
111
- self.assertEqual(out["ids"][0], "old")
112
-
113
-
114
- class EmbeddingFallbackTest(InjectCase):
115
- def test_embedding_provider_failure_degrades_to_lexical(self):
116
- class BadProvider:
117
- def embed_text(self, text):
118
- raise RuntimeError("no network")
119
-
120
- from biocore_memory.engine import MemoryEngine
121
- from biocore_memory.store.json_store import MemoryStore
122
-
123
- def engine_with_bad_embedder(store, entries):
124
- memory = MemoryStore(doc={"entries": [dict(item) for item in entries]})
125
- return MemoryEngine(store=memory, embedder=BadProvider(),
126
- config={"flags": {}})
127
-
128
- saved = pipeline._inject_engine
129
- pipeline._inject_engine = engine_with_bad_embedder
130
- try:
131
- out = self.inject(query="苹果", mode="query", top_k=3)
132
- finally:
133
- pipeline._inject_engine = saved
134
- self.assertEqual(out["ids"][0], "old")
135
-
136
- def test_learning_flags_default_on(self):
137
- captured = {}
138
-
139
- class CaptureEngine:
140
- def __init__(self, **kwargs):
141
- captured.update(kwargs)
142
-
143
- def recall(self, query, budget=5):
144
- return []
145
-
146
- saved = pipeline._inject_engine
147
- saved_engine = None
148
- try:
149
- from biocore_memory.engine import MemoryEngine
150
- saved_engine = MemoryEngine
151
- import biocore_memory.engine as engine_module
152
- engine_module.MemoryEngine = CaptureEngine
153
- try:
154
- pipeline.inject_block("s1", str(self.store), query="苹果")
155
- finally:
156
- engine_module.MemoryEngine = saved_engine
157
- finally:
158
- pipeline._inject_engine = saved
159
- flags = (captured.get("config") or {}).get("flags") or {}
160
- for key in ("mood_recall", "affect_encoding",
161
- "memory_feedback", "preference_drift"):
162
- self.assertIs(flags.get(key), True)
163
-
164
-
165
- class EmbedStatsTest(unittest.TestCase):
166
- def setUp(self):
167
- self.tmp = tempfile.TemporaryDirectory()
168
- self.dir = Path(self.tmp.name)
169
- self.stats_file = self.dir / "embed_stats.json"
170
- self.cache_dir = self.dir / "cache"
171
- self._env = {k: os.environ.get(k) for k in
172
- ("BIOCORE_EMBED_STATS_FILE", "BIOCORE_EMBEDDING_CACHE_FILE")}
173
- os.environ["BIOCORE_EMBED_STATS_FILE"] = str(self.stats_file)
174
- os.environ["BIOCORE_EMBEDDING_CACHE_FILE"] = str(self.cache_dir)
175
- embedding.embed_stats(reset=True)
176
-
177
- def tearDown(self):
178
- for key, value in self._env.items():
179
- if value is None:
180
- os.environ.pop(key, None)
181
- else:
182
- os.environ[key] = value
183
- self.tmp.cleanup()
184
-
185
- def test_miss_remote_then_disk_hit(self):
186
- class Provider:
187
- def embed_text(self, text):
188
- return [0.1] * 8
189
-
190
- self.assertIsNotNone(embedding.embed("hello world", Provider()))
191
- first = json.loads(self.stats_file.read_text(encoding="utf-8"))
192
- self.assertEqual(first["miss"], 1)
193
- self.assertEqual(first["remote"], 1)
194
- self.assertEqual(first["disk_hit"], 0)
195
- embedding.embed("hello world", Provider())
196
- second = json.loads(self.stats_file.read_text(encoding="utf-8"))
197
- self.assertEqual(second["disk_hit"], 1)
198
-
199
- def test_stats_action_reads_persisted_file(self):
200
- self.stats_file.write_text(json.dumps(
201
- {"hit": 2, "disk_hit": 3, "miss": 4, "remote": 5,
202
- "local": 6, "fail": 7}), encoding="utf-8")
203
- store = self.dir / "memory"
204
- out = pipeline.dispatch("stats", {"store": str(store)})
205
- self.assertEqual(out["embed"]["disk_hit"], 3)
206
- self.assertEqual(out["embed"]["miss"], 4)
207
- self.assertEqual(out["embed"]["remote"], 5)
208
-
209
-
210
- if __name__ == "__main__":
211
- unittest.main()
1
+ # -*- coding: utf-8 -*-
2
+ """task-221 (batch 2): query recall injection + embedding stats persistence."""
3
+ import json
4
+ import os
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_memory.recall import embedding
10
+ from biocore_runtime import pipeline
11
+
12
+
13
+ def entry(i, text, ts, importance=0.5):
14
+ return {"id": i, "text": text, "role": "user", "ts": ts,
15
+ "importance": importance, "hits": 1, "scope": "default",
16
+ "memory_confidence": 0.7, "memory_source": "direct"}
17
+
18
+
19
+ class InjectCase(unittest.TestCase):
20
+ def setUp(self):
21
+ self.tmp = tempfile.TemporaryDirectory()
22
+ self.root = Path(self.tmp.name)
23
+ self.store = self.root / "data" / "memory"
24
+ self.entries = [
25
+ entry("new", "今天买了咖啡", 1700000300.0),
26
+ entry("old", "上次说的苹果很甜", 1700000100.0),
27
+ entry("mid", "明天要去公园", 1700000200.0),
28
+ ]
29
+ (self.store / "sessions" / "s1").mkdir(parents=True)
30
+ (self.store / "sessions" / "s1" / "memory.json").write_text(
31
+ json.dumps({"entries": self.entries}), encoding="utf-8")
32
+
33
+ def tearDown(self):
34
+ self.tmp.cleanup()
35
+
36
+ def inject(self, **kwargs):
37
+ return pipeline.inject_block("s1", str(self.store), **kwargs)
38
+
39
+
40
+ class QueryModeTest(InjectCase):
41
+ def test_query_recall_orders_by_query(self):
42
+ out = self.inject(query="苹果", mode="query", top_k=3)
43
+ self.assertEqual(out["ids"][0], "old")
44
+
45
+ def test_recent_mode_keeps_newest_first(self):
46
+ out = self.inject(query="苹果", mode="recent", top_k=3)
47
+ self.assertEqual(out["ids"][0], "new")
48
+
49
+ def test_default_mode_is_query(self):
50
+ out = self.inject(query="苹果", top_k=2)
51
+ self.assertEqual(out["ids"][0], "old")
52
+ self.assertEqual(len(out["ids"]), 2)
53
+
54
+ def test_empty_query_falls_back_recent(self):
55
+ out = self.inject(query="", mode="query", top_k=3)
56
+ self.assertEqual(out["ids"][0], "new")
57
+
58
+ def test_engine_failure_falls_back_recent(self):
59
+ saved = pipeline._inject_engine
60
+ pipeline._inject_engine = lambda store, entries: (_ for _ in ()).throw(RuntimeError("boom"))
61
+ try:
62
+ out = self.inject(query="苹果", mode="query", top_k=2)
63
+ finally:
64
+ pipeline._inject_engine = saved
65
+ self.assertEqual(out["ids"][0], "new")
66
+ self.assertEqual(len(out["ids"]), 2)
67
+
68
+ def test_output_format_and_budget(self):
69
+ out = self.inject(query="苹果", mode="query", top_k=3, max_chars=60)
70
+ self.assertTrue(out["text"].startswith("你记得这些事:"))
71
+ self.assertTrue(all(line.startswith("- ") for line in out["text"].split("\n")[1:]))
72
+ self.assertLessEqual(out["chars"], 60)
73
+ self.assertEqual(out["chars"], len(out["text"]))
74
+ self.assertTrue(set(out["ids"]).issubset({"new", "old", "mid"}))
75
+
76
+
77
+ class RerankTest(InjectCase):
78
+ def test_rerank_reorders_when_enabled(self):
79
+ class FakeRerank:
80
+ def __init__(self):
81
+ self.calls = 0
82
+
83
+ def rerank_documents(self, query, documents, config):
84
+ self.calls += 1
85
+ return [{"index": 2}, {"index": 0}, {"index": 1}]
86
+
87
+ fake = FakeRerank()
88
+ saved = pipeline._rerank_mod
89
+ pipeline._rerank_mod = fake
90
+ try:
91
+ out = self.inject(query="苹果", mode="query", top_k=8,
92
+ rerank_config={"rerank_enabled": True})
93
+ finally:
94
+ pipeline._rerank_mod = saved
95
+ self.assertEqual(fake.calls, 1)
96
+ # engine put 苹果 (old) first; rerank's order must win instead
97
+ self.assertEqual(out["ids"], ["mid", "old", "new"])
98
+
99
+ def test_rerank_off_does_not_call(self):
100
+ class Exploding:
101
+ def rerank_documents(self, *args, **kwargs):
102
+ raise AssertionError("rerank must not run")
103
+
104
+ saved = pipeline._rerank_mod
105
+ pipeline._rerank_mod = Exploding()
106
+ try:
107
+ out = self.inject(query="苹果", mode="query", top_k=8,
108
+ rerank_config={"rerank_enabled": False})
109
+ finally:
110
+ pipeline._rerank_mod = saved
111
+ self.assertEqual(out["ids"][0], "old")
112
+
113
+
114
+ class EmbeddingFallbackTest(InjectCase):
115
+ def test_embedding_provider_failure_degrades_to_lexical(self):
116
+ class BadProvider:
117
+ def embed_text(self, text):
118
+ raise RuntimeError("no network")
119
+
120
+ from biocore_memory.engine import MemoryEngine
121
+ from biocore_memory.store.json_store import MemoryStore
122
+
123
+ def engine_with_bad_embedder(store, entries):
124
+ memory = MemoryStore(doc={"entries": [dict(item) for item in entries]})
125
+ return MemoryEngine(store=memory, embedder=BadProvider(),
126
+ config={"flags": {}})
127
+
128
+ saved = pipeline._inject_engine
129
+ pipeline._inject_engine = engine_with_bad_embedder
130
+ try:
131
+ out = self.inject(query="苹果", mode="query", top_k=3)
132
+ finally:
133
+ pipeline._inject_engine = saved
134
+ self.assertEqual(out["ids"][0], "old")
135
+
136
+ def test_learning_flags_default_on(self):
137
+ captured = {}
138
+
139
+ class CaptureEngine:
140
+ def __init__(self, **kwargs):
141
+ captured.update(kwargs)
142
+
143
+ def recall(self, query, budget=5):
144
+ return []
145
+
146
+ saved = pipeline._inject_engine
147
+ saved_engine = None
148
+ try:
149
+ from biocore_memory.engine import MemoryEngine
150
+ saved_engine = MemoryEngine
151
+ import biocore_memory.engine as engine_module
152
+ engine_module.MemoryEngine = CaptureEngine
153
+ try:
154
+ pipeline.inject_block("s1", str(self.store), query="苹果")
155
+ finally:
156
+ engine_module.MemoryEngine = saved_engine
157
+ finally:
158
+ pipeline._inject_engine = saved
159
+ flags = (captured.get("config") or {}).get("flags") or {}
160
+ for key in ("mood_recall", "affect_encoding",
161
+ "memory_feedback", "preference_drift"):
162
+ self.assertIs(flags.get(key), True)
163
+
164
+
165
+ class EmbedStatsTest(unittest.TestCase):
166
+ def setUp(self):
167
+ self.tmp = tempfile.TemporaryDirectory()
168
+ self.dir = Path(self.tmp.name)
169
+ self.stats_file = self.dir / "embed_stats.json"
170
+ self.cache_dir = self.dir / "cache"
171
+ self._env = {k: os.environ.get(k) for k in
172
+ ("BIOCORE_EMBED_STATS_FILE", "BIOCORE_EMBEDDING_CACHE_FILE")}
173
+ os.environ["BIOCORE_EMBED_STATS_FILE"] = str(self.stats_file)
174
+ os.environ["BIOCORE_EMBEDDING_CACHE_FILE"] = str(self.cache_dir)
175
+ embedding.embed_stats(reset=True)
176
+
177
+ def tearDown(self):
178
+ for key, value in self._env.items():
179
+ if value is None:
180
+ os.environ.pop(key, None)
181
+ else:
182
+ os.environ[key] = value
183
+ self.tmp.cleanup()
184
+
185
+ def test_miss_remote_then_disk_hit(self):
186
+ class Provider:
187
+ def embed_text(self, text):
188
+ return [0.1] * 8
189
+
190
+ self.assertIsNotNone(embedding.embed("hello world", Provider()))
191
+ first = json.loads(self.stats_file.read_text(encoding="utf-8"))
192
+ self.assertEqual(first["miss"], 1)
193
+ self.assertEqual(first["remote"], 1)
194
+ self.assertEqual(first["disk_hit"], 0)
195
+ embedding.embed("hello world", Provider())
196
+ second = json.loads(self.stats_file.read_text(encoding="utf-8"))
197
+ self.assertEqual(second["disk_hit"], 1)
198
+
199
+ def test_stats_action_reads_persisted_file(self):
200
+ self.stats_file.write_text(json.dumps(
201
+ {"hit": 2, "disk_hit": 3, "miss": 4, "remote": 5,
202
+ "local": 6, "fail": 7}), encoding="utf-8")
203
+ store = self.dir / "memory"
204
+ out = pipeline.dispatch("stats", {"store": str(store)})
205
+ self.assertEqual(out["embed"]["disk_hit"], 3)
206
+ self.assertEqual(out["embed"]["miss"], 4)
207
+ self.assertEqual(out["embed"]["remote"], 5)
208
+
209
+
210
+ if __name__ == "__main__":
211
+ unittest.main()
@@ -0,0 +1,143 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-62: normalized channels, chunks, shards, predictive exploration."""
3
+ import json
4
+ import tempfile
5
+ import time
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_runtime import pipeline, retrieval_plus as rp
10
+
11
+
12
+ def entry(i, text, ts=None):
13
+ return {"id": i, "text": text, "role": "user",
14
+ "ts": ts if ts is not None else time.time()}
15
+
16
+
17
+ class Case(unittest.TestCase):
18
+ def setUp(self):
19
+ self.tmp = tempfile.TemporaryDirectory()
20
+ self.data_dir = Path(self.tmp.name) / "data"
21
+ self.store = self.data_dir / "memory"
22
+ (self.store / "sessions" / "s1").mkdir(parents=True)
23
+ self.now = time.time()
24
+
25
+ def tearDown(self):
26
+ self.tmp.cleanup()
27
+
28
+
29
+ class NormalizeTest(Case):
30
+ def test_min_max(self):
31
+ self.assertEqual(rp.normalize([0.0, 5.0, 10.0]), [0.0, 0.5, 1.0])
32
+
33
+ def test_flat_positive_is_one(self):
34
+ self.assertEqual(rp.normalize([2.0, 2.0]), [1.0, 1.0])
35
+
36
+ def test_flat_zero_is_zero(self):
37
+ self.assertEqual(rp.normalize([0.0, 0.0]), [0.0, 0.0])
38
+
39
+ def test_channels_are_independent(self):
40
+ rows = [entry("a", "苹果 苹果 苹果"), entry("b", "香蕉")]
41
+ ranked = rp.rank(self.store, "s1", "苹果", rows, now=self.now)
42
+ by_id = {item["entry"]["id"]: item for item in ranked}
43
+ self.assertEqual(by_id["a"]["channels"]["bm25"], 1.0)
44
+ self.assertEqual(by_id["b"]["channels"]["bm25"], 0.0)
45
+ self.assertIn("vector", by_id["a"]["channels"])
46
+
47
+
48
+ class ChunkTest(Case):
49
+ def test_sentences_split(self):
50
+ chunks = rp.split_chunks("第一句。第二句!第三句?")
51
+ self.assertEqual(len(chunks), 3)
52
+
53
+ def test_index_persists(self):
54
+ rp.build_chunks(self.store, [entry("a", "第一句。第二句。")])
55
+ doc = json.loads((self.data_dir / "retrieval" / "chunks.json")
56
+ .read_text(encoding="utf-8"))
57
+ self.assertIn("a", doc["entries"])
58
+ self.assertEqual(len(doc["entries"]["a"]["chunks"]), 2)
59
+
60
+ def test_best_chunk_snippet(self):
61
+ rows = [entry("a", "今天很累。苹果很甜,想再买一次。")]
62
+ ranked = rp.rank(self.store, "s1", "苹果", rows, now=self.now)
63
+ self.assertIn("苹果", ranked[0]["chunk"])
64
+ self.assertGreaterEqual(ranked[0]["chunk_hits"], 1)
65
+
66
+
67
+ class ShardTest(Case):
68
+ def test_session_shard(self):
69
+ shards = rp.assign_shards(self.store, [entry("a", "x")], session_id="s1")
70
+ self.assertEqual(shards["a"], "s-s1")
71
+ self.assertTrue((self.data_dir / "retrieval" / "shards"
72
+ / "s-s1.json").exists())
73
+
74
+ def test_month_shard_without_session(self):
75
+ row = entry("a", "x", ts=time.mktime((2024, 3, 15, 12, 0, 0, 0, 0, -1)))
76
+ shards = rp.assign_shards(self.store, [row])
77
+ self.assertEqual(shards["a"], "2024-03")
78
+
79
+ def test_rollover_over_cap(self):
80
+ rows = [entry(str(index), "x") for index in range(5)]
81
+ shards = rp.assign_shards(self.store, rows, session_id="s1", limit=2)
82
+ names = list(shards.values())
83
+ self.assertEqual(names.count("s-s1"), 2)
84
+ self.assertIn("s-s1-r1", names)
85
+ self.assertIn("s-s1-r2", names)
86
+
87
+
88
+ class PredictionTest(Case):
89
+ def test_expectation_weights(self):
90
+ rp.record_query(self.store, "苹果 咖啡")
91
+ rp.record_query(self.store, "苹果 咖啡 甜")
92
+ rows = [entry("expected", "苹果 咖啡"), entry("novel", "维修 螺丝")]
93
+ ranked = rp.rank(self.store, "s1", "苹果", rows, now=self.now)
94
+ by_id = {item["entry"]["id"]: item for item in ranked}
95
+ self.assertGreater(by_id["novel"]["expectation_weight"],
96
+ by_id["expected"]["expectation_weight"])
97
+
98
+ def test_topic_history_bounded(self):
99
+ for index in range(25):
100
+ rp.record_query(self.store, "话题%d" % index)
101
+ doc = json.loads((self.data_dir / "topic_history.json")
102
+ .read_text(encoding="utf-8"))
103
+ self.assertEqual(len(doc["turns"]), 20)
104
+
105
+ def test_off_fallback(self):
106
+ rows = [entry("a", "苹果"), entry("b", "香蕉")]
107
+ ranked = rp.rank(self.store, "s1", "苹果", rows, now=self.now,
108
+ enabled=False)
109
+ self.assertEqual([item["entry"]["id"] for item in ranked], ["a", "b"])
110
+ self.assertEqual(ranked[0]["reason"], "fallback")
111
+
112
+
113
+ class DebugTest(Case):
114
+ def test_debug_rows_and_state(self):
115
+ rows = [entry("a", "苹果很甜"), entry("b", "香蕉")]
116
+ ranked = rp.rank(self.store, "s1", "苹果", rows, now=self.now)
117
+ rp.record_debug(self.store, "苹果", ranked, now=self.now)
118
+ summary = rp.summary(self.store)
119
+ self.assertEqual(summary["query"], "苹果")
120
+ for key in ("channels", "reason", "chunk", "chunk_hits", "shard"):
121
+ self.assertIn(key, summary["rows"][0])
122
+ body = pipeline.dispatch("state", {
123
+ "store": str(self.store), "session_id": "s1",
124
+ "persona_profile": str(Path(self.tmp.name) / "missing.json")})["state"]
125
+ self.assertIn("recall_debug", body)
126
+
127
+ def test_switch_off_keeps_engine_order(self):
128
+ rows = [entry("old", "苹果 很甜", ts=self.now - 100),
129
+ entry("new", "苹果 今天", ts=self.now)]
130
+ (self.store / "sessions" / "s1" / "memory.json").write_text(
131
+ json.dumps({"entries": rows}), encoding="utf-8")
132
+ on = pipeline.inject_block("s1", str(self.store), query="苹果",
133
+ mode="query", top_k=2)
134
+ path = self.data_dir / "core_config.json"
135
+ path.write_text(json.dumps({"switches": {"retrieval_plus": False}}),
136
+ encoding="utf-8")
137
+ off = pipeline.inject_block("s1", str(self.store), query="苹果",
138
+ mode="query", top_k=2)
139
+ self.assertEqual(len(on["ids"]), len(off["ids"]))
140
+
141
+
142
+ if __name__ == "__main__":
143
+ unittest.main()