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.
- package/lib/index.js +30 -1
- package/lib/modules/standby.js +61 -0
- package/lib/modules/state.js +22 -3
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +7 -3
- package/python/biocore_memory/engine.py +91 -6
- package/python/biocore_memory/recall/embedding.py +0 -51
- package/python/biocore_memory/recall/feedback.py +34 -7
- package/python/biocore_memory/recall/fusion.py +5 -1
- package/python/biocore_memory/scoring.py +203 -0
- package/python/biocore_memory/space.py +355 -0
- package/python/biocore_memory/store/dedupe.py +1 -0
- package/python/biocore_memory/store/tiering.py +14 -3
- package/python/biocore_model/affective_labor.py +14 -1
- package/python/biocore_model/appraisal.py +172 -3
- package/python/biocore_model/bio_tick.py +43 -13
- package/python/biocore_model/emotion_geometry.py +9 -0
- package/python/biocore_model/envy.py +21 -0
- package/python/biocore_model/feedback_memory.py +53 -0
- package/python/biocore_model/needs_events.py +32 -11
- package/python/biocore_model/regulation_strategies.py +5 -0
- package/python/biocore_model/repair_after_boundary.py +1 -0
- package/python/biocore_model/self_esteem.py +14 -2
- package/python/biocore_model/self_intent.py +6 -4
- package/python/biocore_model/tests/test_autonomy_self.py +153 -153
- package/python/biocore_model/tests/test_cache_series.py +137 -0
- package/python/biocore_model/tests/test_core_state.py +240 -240
- package/python/biocore_model/tests/test_extensions.py +167 -167
- package/python/biocore_model/tests/test_field_arbiter.py +444 -0
- package/python/biocore_model/tests/test_governance.py +181 -181
- package/python/biocore_model/tests/test_lifecycle.py +153 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -211
- package/python/biocore_model/tests/test_retrieval_plus.py +143 -0
- package/python/biocore_model/tests/test_signals.py +106 -106
- package/python/biocore_model/tests/test_standby.py +199 -0
- package/python/biocore_runtime/__init__.py +3 -0
- package/python/biocore_runtime/arbiter.py +307 -0
- package/python/biocore_runtime/batch_b.py +724 -0
- package/python/biocore_runtime/batch_c.py +186 -0
- package/python/biocore_runtime/batch_d.py +659 -0
- package/python/biocore_runtime/batch_e.py +328 -0
- package/python/biocore_runtime/batch_registry.py +446 -0
- package/python/biocore_runtime/cache_stats.py +161 -0
- package/python/biocore_runtime/core_state.py +106 -3
- package/python/biocore_runtime/dimensions.py +419 -0
- package/python/biocore_runtime/expression.py +261 -0
- package/python/biocore_runtime/field.py +440 -0
- package/python/biocore_runtime/lifecycle.py +233 -0
- package/python/biocore_runtime/perception.py +1045 -0
- package/python/biocore_runtime/pipeline.py +4458 -2974
- package/python/biocore_runtime/priority.py +210 -0
- package/python/biocore_runtime/retrieval_plus.py +352 -0
- package/python/biocore_runtime/standby.py +581 -472
- package/python/biocore_runtime/timescales.py +263 -0
- package/python/data/signals.json +3266 -206
- package/ui/panel-layout.js +32 -29
- package/ui/panel.css +563 -974
- package/ui/panel.html +427 -401
- 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()
|