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,181 +1,181 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""task-223: memory governance (decay/index/consolidate/concepts) + relationship."""
|
|
3
|
-
import json
|
|
4
|
-
import tempfile
|
|
5
|
-
import time
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from biocore_runtime import governance
|
|
10
|
-
from biocore_runtime import pipeline
|
|
11
|
-
from biocore_runtime import relationship_state
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def write_json(path, data):
|
|
15
|
-
path = Path(path)
|
|
16
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
-
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def memory(entry_id, text, ts, importance=0.5, concepts=None):
|
|
21
|
-
row = {"id": entry_id, "text": text, "role": "user", "ts": ts,
|
|
22
|
-
"importance": importance, "hits": 1}
|
|
23
|
-
if concepts is not None:
|
|
24
|
-
row["concepts"] = list(concepts)
|
|
25
|
-
return row
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class StoreCase(unittest.TestCase):
|
|
29
|
-
NOW = 1700000000.0
|
|
30
|
-
|
|
31
|
-
def setUp(self):
|
|
32
|
-
self.tmp = tempfile.TemporaryDirectory()
|
|
33
|
-
self.data_dir = Path(self.tmp.name) / "data"
|
|
34
|
-
self.store = self.data_dir / "memory"
|
|
35
|
-
self.persona = Path(self.tmp.name) / "missing_persona.json"
|
|
36
|
-
self.write_session("s1", [
|
|
37
|
-
memory("fresh", "今天买了咖啡", self.NOW - 60, 0.9),
|
|
38
|
-
memory("old", "很久以前的事", self.NOW - 400 * 86400, 0.2),
|
|
39
|
-
memory("mid", "苹果很甜", self.NOW - 86400, 0.5),
|
|
40
|
-
])
|
|
41
|
-
|
|
42
|
-
def tearDown(self):
|
|
43
|
-
self.tmp.cleanup()
|
|
44
|
-
|
|
45
|
-
def write_session(self, session_id, entries):
|
|
46
|
-
write_json(self.store / "sessions" / session_id / "memory.json",
|
|
47
|
-
{"session_id": session_id, "entries": entries})
|
|
48
|
-
|
|
49
|
-
def payload(self, session_id="s1", **extra):
|
|
50
|
-
data = {"store": str(self.store), "session_id": session_id,
|
|
51
|
-
"persona_profile": str(self.persona)}
|
|
52
|
-
data.update(extra)
|
|
53
|
-
return data
|
|
54
|
-
|
|
55
|
-
def turn_end(self, session_id="s1", user_text="", **extra):
|
|
56
|
-
return pipeline.dispatch("turn_end", self.payload(
|
|
57
|
-
session_id, user_text=user_text, assistant_text="嗯。",
|
|
58
|
-
capture=False, **extra))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class DecayTest(StoreCase):
|
|
62
|
-
def test_decay_marks_but_never_deletes(self):
|
|
63
|
-
before = len(governance._entries(self.store))
|
|
64
|
-
out = governance.run(self.store, now=self.NOW, force=True)
|
|
65
|
-
after = len(governance._entries(self.store))
|
|
66
|
-
self.assertEqual(before, after)
|
|
67
|
-
self.assertGreaterEqual(out["decay"]["cold"], 1)
|
|
68
|
-
self.assertEqual(out["decay"]["deleted"], 0)
|
|
69
|
-
self.assertLess(governance.weight_of(self.store, "old"), 1.0)
|
|
70
|
-
|
|
71
|
-
def test_fresh_entry_keeps_full_weight(self):
|
|
72
|
-
governance.run(self.store, now=self.NOW, force=True)
|
|
73
|
-
self.assertEqual(governance.weight_of(self.store, "fresh"), 1.0)
|
|
74
|
-
|
|
75
|
-
def test_evict_delete_flag_does_not_delete(self):
|
|
76
|
-
write_json(self.data_dir / "flags.json", {"evict_delete": True})
|
|
77
|
-
before = len(governance._entries(self.store))
|
|
78
|
-
governance.run(self.store, now=self.NOW, force=True)
|
|
79
|
-
self.assertEqual(len(governance._entries(self.store)), before)
|
|
80
|
-
|
|
81
|
-
def test_governance_off_skips(self):
|
|
82
|
-
write_json(self.data_dir / "flags.json", {"governance": False})
|
|
83
|
-
out = governance.run(self.store, now=self.NOW, force=True)
|
|
84
|
-
self.assertTrue(out.get("skipped"))
|
|
85
|
-
self.assertFalse((self.store / "governance.json").exists())
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class IndexAndConceptTest(StoreCase):
|
|
89
|
-
def test_index_lookup_hits_entry(self):
|
|
90
|
-
governance.run(self.store, now=self.NOW, force=True)
|
|
91
|
-
self.assertIn("mid", governance.lookup(self.store, "苹果"))
|
|
92
|
-
doc = json.loads((self.store / "index.json").read_text(encoding="utf-8"))
|
|
93
|
-
self.assertTrue(doc["terms"])
|
|
94
|
-
|
|
95
|
-
def test_inject_uses_index_and_counts_hit(self):
|
|
96
|
-
governance.run(self.store, now=self.NOW, force=True)
|
|
97
|
-
out = pipeline.inject_block("s1", str(self.store), query="苹果",
|
|
98
|
-
mode="query", top_k=3)
|
|
99
|
-
self.assertEqual(out["ids"][0], "mid")
|
|
100
|
-
gov = json.loads((self.store / "governance.json").read_text(encoding="utf-8"))
|
|
101
|
-
self.assertGreaterEqual(gov.get("index_hits", 0), 1)
|
|
102
|
-
|
|
103
|
-
def test_remember_stores_concepts(self):
|
|
104
|
-
pipeline.tool_call("remember", {"text": "苹果和咖啡都很好"},
|
|
105
|
-
{"root": str(self.store)}, session_id="s2")
|
|
106
|
-
doc = json.loads((self.store / "sessions" / "s2" / "memory.json")
|
|
107
|
-
.read_text(encoding="utf-8"))
|
|
108
|
-
self.assertTrue(doc["entries"][0].get("concepts"))
|
|
109
|
-
|
|
110
|
-
def test_concept_filter_narrows_candidates(self):
|
|
111
|
-
entries = [{"id": "a", "concepts": ["苹果", "甜"]},
|
|
112
|
-
{"id": "b", "concepts": ["香蕉", "黄"]}]
|
|
113
|
-
kept = pipeline._concept_filter(entries, "苹果")
|
|
114
|
-
self.assertEqual([row["id"] for row in kept], ["a"])
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class ConsolidateTest(StoreCase):
|
|
118
|
-
def test_consolidate_is_idempotent(self):
|
|
119
|
-
self.write_session("s2", [
|
|
120
|
-
memory("c1", "注意安全", self.NOW - 3),
|
|
121
|
-
memory("c2", "出门注意安全", self.NOW - 2),
|
|
122
|
-
memory("c3", "记得注意安全", self.NOW - 1),
|
|
123
|
-
])
|
|
124
|
-
first = governance.run(self.store, now=self.NOW, force=True)
|
|
125
|
-
second = governance.run(self.store, now=self.NOW + 1, force=True)
|
|
126
|
-
self.assertEqual(first["consolidate"]["candidates"],
|
|
127
|
-
second["consolidate"]["candidates"])
|
|
128
|
-
self.assertTrue(second["consolidate"]["candidates"] >= 1)
|
|
129
|
-
self.assertFalse(second["consolidate"]["changed"])
|
|
130
|
-
doc = json.loads((self.store / "consolidate.json").read_text(encoding="utf-8"))
|
|
131
|
-
self.assertGreaterEqual(doc["runs"], 2)
|
|
132
|
-
|
|
133
|
-
def test_graph_and_aggregate(self):
|
|
134
|
-
self.write_session("s2", [memory("g1", "苹果 苹果 甜", self.NOW - 5),
|
|
135
|
-
memory("g2", "苹果 很甜", self.NOW - 4)])
|
|
136
|
-
out = governance.run(self.store, now=self.NOW, force=True)
|
|
137
|
-
self.assertGreaterEqual(out["graph"]["nodes"], 1)
|
|
138
|
-
self.assertGreaterEqual(out["aggregate"]["sessions"], 2)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class RelationshipTest(StoreCase):
|
|
142
|
-
def test_warm_text_raises_closeness(self):
|
|
143
|
-
self.turn_end(user_text="谢谢你陪我,今天很开心")
|
|
144
|
-
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
145
|
-
self.assertGreater(rel["relationship"]["closeness"], 0.5)
|
|
146
|
-
self.assertGreater(rel["relationship"]["trust"], 0.5)
|
|
147
|
-
|
|
148
|
-
def test_conflict_text_raises_conflict(self):
|
|
149
|
-
self.turn_end(user_text="我很生气,别说了")
|
|
150
|
-
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
151
|
-
self.assertGreater(rel["relationship"]["conflict"], 0.0)
|
|
152
|
-
self.assertLess(rel["relationship"]["trust"], 0.5)
|
|
153
|
-
|
|
154
|
-
def test_repair_lowers_conflict(self):
|
|
155
|
-
self.turn_end(user_text="我很生气")
|
|
156
|
-
self.turn_end(user_text="对不起,是我不好")
|
|
157
|
-
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
158
|
-
summary = relationship_state.summary(self.store, rel["relationship"])
|
|
159
|
-
self.assertGreaterEqual(summary["repairs"], 1)
|
|
160
|
-
self.assertLess(rel["relationship"]["conflict"], 0.02)
|
|
161
|
-
|
|
162
|
-
def test_relationship_off_keeps_values(self):
|
|
163
|
-
write_json(self.data_dir / "flags.json", {"relationship_governance": False})
|
|
164
|
-
self.turn_end(user_text="谢谢你陪我")
|
|
165
|
-
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
166
|
-
self.assertAlmostEqual(rel["relationship"]["closeness"], 0.5, places=4)
|
|
167
|
-
self.assertAlmostEqual(rel["relationship"]["trust"], 0.5, places=4)
|
|
168
|
-
self.assertFalse((self.store / "relationship_memory.json").exists())
|
|
169
|
-
|
|
170
|
-
def test_state_exposes_numbers(self):
|
|
171
|
-
self.turn_end(user_text="谢谢你陪我")
|
|
172
|
-
out = pipeline.dispatch("state", self.payload())
|
|
173
|
-
body = out["state"]
|
|
174
|
-
self.assertIn("governance", body)
|
|
175
|
-
self.assertIn("decay", body["governance"])
|
|
176
|
-
self.assertIn("relationship_summary", body)
|
|
177
|
-
self.assertGreaterEqual(body["relationship_summary"]["events"], 1)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if __name__ == "__main__":
|
|
181
|
-
unittest.main()
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-223: memory governance (decay/index/consolidate/concepts) + relationship."""
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_runtime import governance
|
|
10
|
+
from biocore_runtime import pipeline
|
|
11
|
+
from biocore_runtime import relationship_state
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_json(path, data):
|
|
15
|
+
path = Path(path)
|
|
16
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def memory(entry_id, text, ts, importance=0.5, concepts=None):
|
|
21
|
+
row = {"id": entry_id, "text": text, "role": "user", "ts": ts,
|
|
22
|
+
"importance": importance, "hits": 1}
|
|
23
|
+
if concepts is not None:
|
|
24
|
+
row["concepts"] = list(concepts)
|
|
25
|
+
return row
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StoreCase(unittest.TestCase):
|
|
29
|
+
NOW = 1700000000.0
|
|
30
|
+
|
|
31
|
+
def setUp(self):
|
|
32
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
33
|
+
self.data_dir = Path(self.tmp.name) / "data"
|
|
34
|
+
self.store = self.data_dir / "memory"
|
|
35
|
+
self.persona = Path(self.tmp.name) / "missing_persona.json"
|
|
36
|
+
self.write_session("s1", [
|
|
37
|
+
memory("fresh", "今天买了咖啡", self.NOW - 60, 0.9),
|
|
38
|
+
memory("old", "很久以前的事", self.NOW - 400 * 86400, 0.2),
|
|
39
|
+
memory("mid", "苹果很甜", self.NOW - 86400, 0.5),
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
def tearDown(self):
|
|
43
|
+
self.tmp.cleanup()
|
|
44
|
+
|
|
45
|
+
def write_session(self, session_id, entries):
|
|
46
|
+
write_json(self.store / "sessions" / session_id / "memory.json",
|
|
47
|
+
{"session_id": session_id, "entries": entries})
|
|
48
|
+
|
|
49
|
+
def payload(self, session_id="s1", **extra):
|
|
50
|
+
data = {"store": str(self.store), "session_id": session_id,
|
|
51
|
+
"persona_profile": str(self.persona)}
|
|
52
|
+
data.update(extra)
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
def turn_end(self, session_id="s1", user_text="", **extra):
|
|
56
|
+
return pipeline.dispatch("turn_end", self.payload(
|
|
57
|
+
session_id, user_text=user_text, assistant_text="嗯。",
|
|
58
|
+
capture=False, **extra))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DecayTest(StoreCase):
|
|
62
|
+
def test_decay_marks_but_never_deletes(self):
|
|
63
|
+
before = len(governance._entries(self.store))
|
|
64
|
+
out = governance.run(self.store, now=self.NOW, force=True)
|
|
65
|
+
after = len(governance._entries(self.store))
|
|
66
|
+
self.assertEqual(before, after)
|
|
67
|
+
self.assertGreaterEqual(out["decay"]["cold"], 1)
|
|
68
|
+
self.assertEqual(out["decay"]["deleted"], 0)
|
|
69
|
+
self.assertLess(governance.weight_of(self.store, "old"), 1.0)
|
|
70
|
+
|
|
71
|
+
def test_fresh_entry_keeps_full_weight(self):
|
|
72
|
+
governance.run(self.store, now=self.NOW, force=True)
|
|
73
|
+
self.assertEqual(governance.weight_of(self.store, "fresh"), 1.0)
|
|
74
|
+
|
|
75
|
+
def test_evict_delete_flag_does_not_delete(self):
|
|
76
|
+
write_json(self.data_dir / "flags.json", {"evict_delete": True})
|
|
77
|
+
before = len(governance._entries(self.store))
|
|
78
|
+
governance.run(self.store, now=self.NOW, force=True)
|
|
79
|
+
self.assertEqual(len(governance._entries(self.store)), before)
|
|
80
|
+
|
|
81
|
+
def test_governance_off_skips(self):
|
|
82
|
+
write_json(self.data_dir / "flags.json", {"governance": False})
|
|
83
|
+
out = governance.run(self.store, now=self.NOW, force=True)
|
|
84
|
+
self.assertTrue(out.get("skipped"))
|
|
85
|
+
self.assertFalse((self.store / "governance.json").exists())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class IndexAndConceptTest(StoreCase):
|
|
89
|
+
def test_index_lookup_hits_entry(self):
|
|
90
|
+
governance.run(self.store, now=self.NOW, force=True)
|
|
91
|
+
self.assertIn("mid", governance.lookup(self.store, "苹果"))
|
|
92
|
+
doc = json.loads((self.store / "index.json").read_text(encoding="utf-8"))
|
|
93
|
+
self.assertTrue(doc["terms"])
|
|
94
|
+
|
|
95
|
+
def test_inject_uses_index_and_counts_hit(self):
|
|
96
|
+
governance.run(self.store, now=self.NOW, force=True)
|
|
97
|
+
out = pipeline.inject_block("s1", str(self.store), query="苹果",
|
|
98
|
+
mode="query", top_k=3)
|
|
99
|
+
self.assertEqual(out["ids"][0], "mid")
|
|
100
|
+
gov = json.loads((self.store / "governance.json").read_text(encoding="utf-8"))
|
|
101
|
+
self.assertGreaterEqual(gov.get("index_hits", 0), 1)
|
|
102
|
+
|
|
103
|
+
def test_remember_stores_concepts(self):
|
|
104
|
+
pipeline.tool_call("remember", {"text": "苹果和咖啡都很好"},
|
|
105
|
+
{"root": str(self.store)}, session_id="s2")
|
|
106
|
+
doc = json.loads((self.store / "sessions" / "s2" / "memory.json")
|
|
107
|
+
.read_text(encoding="utf-8"))
|
|
108
|
+
self.assertTrue(doc["entries"][0].get("concepts"))
|
|
109
|
+
|
|
110
|
+
def test_concept_filter_narrows_candidates(self):
|
|
111
|
+
entries = [{"id": "a", "concepts": ["苹果", "甜"]},
|
|
112
|
+
{"id": "b", "concepts": ["香蕉", "黄"]}]
|
|
113
|
+
kept = pipeline._concept_filter(entries, "苹果")
|
|
114
|
+
self.assertEqual([row["id"] for row in kept], ["a"])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ConsolidateTest(StoreCase):
|
|
118
|
+
def test_consolidate_is_idempotent(self):
|
|
119
|
+
self.write_session("s2", [
|
|
120
|
+
memory("c1", "注意安全", self.NOW - 3),
|
|
121
|
+
memory("c2", "出门注意安全", self.NOW - 2),
|
|
122
|
+
memory("c3", "记得注意安全", self.NOW - 1),
|
|
123
|
+
])
|
|
124
|
+
first = governance.run(self.store, now=self.NOW, force=True)
|
|
125
|
+
second = governance.run(self.store, now=self.NOW + 1, force=True)
|
|
126
|
+
self.assertEqual(first["consolidate"]["candidates"],
|
|
127
|
+
second["consolidate"]["candidates"])
|
|
128
|
+
self.assertTrue(second["consolidate"]["candidates"] >= 1)
|
|
129
|
+
self.assertFalse(second["consolidate"]["changed"])
|
|
130
|
+
doc = json.loads((self.store / "consolidate.json").read_text(encoding="utf-8"))
|
|
131
|
+
self.assertGreaterEqual(doc["runs"], 2)
|
|
132
|
+
|
|
133
|
+
def test_graph_and_aggregate(self):
|
|
134
|
+
self.write_session("s2", [memory("g1", "苹果 苹果 甜", self.NOW - 5),
|
|
135
|
+
memory("g2", "苹果 很甜", self.NOW - 4)])
|
|
136
|
+
out = governance.run(self.store, now=self.NOW, force=True)
|
|
137
|
+
self.assertGreaterEqual(out["graph"]["nodes"], 1)
|
|
138
|
+
self.assertGreaterEqual(out["aggregate"]["sessions"], 2)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class RelationshipTest(StoreCase):
|
|
142
|
+
def test_warm_text_raises_closeness(self):
|
|
143
|
+
self.turn_end(user_text="谢谢你陪我,今天很开心")
|
|
144
|
+
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
145
|
+
self.assertGreater(rel["relationship"]["closeness"], 0.5)
|
|
146
|
+
self.assertGreater(rel["relationship"]["trust"], 0.5)
|
|
147
|
+
|
|
148
|
+
def test_conflict_text_raises_conflict(self):
|
|
149
|
+
self.turn_end(user_text="我很生气,别说了")
|
|
150
|
+
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
151
|
+
self.assertGreater(rel["relationship"]["conflict"], 0.0)
|
|
152
|
+
self.assertLess(rel["relationship"]["trust"], 0.5)
|
|
153
|
+
|
|
154
|
+
def test_repair_lowers_conflict(self):
|
|
155
|
+
self.turn_end(user_text="我很生气")
|
|
156
|
+
self.turn_end(user_text="对不起,是我不好")
|
|
157
|
+
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
158
|
+
summary = relationship_state.summary(self.store, rel["relationship"])
|
|
159
|
+
self.assertGreaterEqual(summary["repairs"], 1)
|
|
160
|
+
self.assertLess(rel["relationship"]["conflict"], 0.02)
|
|
161
|
+
|
|
162
|
+
def test_relationship_off_keeps_values(self):
|
|
163
|
+
write_json(self.data_dir / "flags.json", {"relationship_governance": False})
|
|
164
|
+
self.turn_end(user_text="谢谢你陪我")
|
|
165
|
+
rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
|
|
166
|
+
self.assertAlmostEqual(rel["relationship"]["closeness"], 0.5, places=4)
|
|
167
|
+
self.assertAlmostEqual(rel["relationship"]["trust"], 0.5, places=4)
|
|
168
|
+
self.assertFalse((self.store / "relationship_memory.json").exists())
|
|
169
|
+
|
|
170
|
+
def test_state_exposes_numbers(self):
|
|
171
|
+
self.turn_end(user_text="谢谢你陪我")
|
|
172
|
+
out = pipeline.dispatch("state", self.payload())
|
|
173
|
+
body = out["state"]
|
|
174
|
+
self.assertIn("governance", body)
|
|
175
|
+
self.assertIn("decay", body["governance"])
|
|
176
|
+
self.assertIn("relationship_summary", body)
|
|
177
|
+
self.assertGreaterEqual(body["relationship_summary"]["events"], 1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
unittest.main()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-36 + task-78: tier hysteresis, reconsolidation, minimal repair loop."""
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_memory.store import tiering
|
|
10
|
+
from biocore_runtime import lifecycle, pipeline
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_json(path, data):
|
|
14
|
+
path = Path(path)
|
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def entry(i, text="记得这件事", ts=None, hits=1, importance=0.5):
|
|
20
|
+
return {"id": i, "text": text, "role": "user",
|
|
21
|
+
"ts": ts or (time.time() - 3600), "importance": importance,
|
|
22
|
+
"hits": hits}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Case(unittest.TestCase):
|
|
26
|
+
def setUp(self):
|
|
27
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
28
|
+
self.data_dir = Path(self.tmp.name) / "data"
|
|
29
|
+
self.store = self.data_dir / "memory"
|
|
30
|
+
(self.store / "sessions" / "s1").mkdir(parents=True)
|
|
31
|
+
self.persona = Path(self.tmp.name) / "missing.json"
|
|
32
|
+
|
|
33
|
+
def tearDown(self):
|
|
34
|
+
self.tmp.cleanup()
|
|
35
|
+
|
|
36
|
+
def write_entries(self, entries):
|
|
37
|
+
write_json(self.store / "sessions" / "s1" / "memory.json",
|
|
38
|
+
{"entries": entries})
|
|
39
|
+
return entries
|
|
40
|
+
|
|
41
|
+
def read_entries(self):
|
|
42
|
+
return json.loads((self.store / "sessions" / "s1" / "memory.json")
|
|
43
|
+
.read_text(encoding="utf-8"))["entries"]
|
|
44
|
+
|
|
45
|
+
def payload(self, **extra):
|
|
46
|
+
data = {"store": str(self.store), "session_id": "s1",
|
|
47
|
+
"persona_profile": str(self.persona)}
|
|
48
|
+
data.update(extra)
|
|
49
|
+
return data
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TierTest(Case):
|
|
53
|
+
def test_enter_hot_at_five_hits(self):
|
|
54
|
+
self.assertEqual(tiering.tier_of(entry("a", hits=5), now=time.time()),
|
|
55
|
+
"hot")
|
|
56
|
+
|
|
57
|
+
def test_stays_hot_between_three_and_five(self):
|
|
58
|
+
now = time.time()
|
|
59
|
+
self.assertEqual(tiering.tier_of(entry("a", hits=4), now=now,
|
|
60
|
+
previous="hot"), "hot")
|
|
61
|
+
|
|
62
|
+
def test_exits_hot_below_three(self):
|
|
63
|
+
now = time.time()
|
|
64
|
+
self.assertNotEqual(tiering.tier_of(entry("a", hits=2), now=now,
|
|
65
|
+
previous="hot"), "hot")
|
|
66
|
+
|
|
67
|
+
def test_tick_counts_entered_exited(self):
|
|
68
|
+
now = time.time()
|
|
69
|
+
old_ts = time.time() - 3 * 86400
|
|
70
|
+
rows = [entry("a", hits=5), entry("b", hits=2, ts=old_ts)]
|
|
71
|
+
first = lifecycle.tier_tick(self.store, rows, now=now)
|
|
72
|
+
self.assertEqual(first["counts"]["hot"], 1)
|
|
73
|
+
rows[0]["hits"] = 1
|
|
74
|
+
second = lifecycle.tier_tick(self.store, rows, now=now + 1)
|
|
75
|
+
self.assertGreaterEqual(second["exited"], 1)
|
|
76
|
+
summary = lifecycle.tier_summary(self.store)
|
|
77
|
+
self.assertIn("hot", summary)
|
|
78
|
+
self.assertIn("exited", summary)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ReconsolidationTest(Case):
|
|
82
|
+
def test_updates_recalled_entry(self):
|
|
83
|
+
now = time.time()
|
|
84
|
+
self.write_entries([entry("a", "苹果很甜", hits=1)])
|
|
85
|
+
lifecycle.record_recall(self.store, "s1", ["a"], now=now)
|
|
86
|
+
result = lifecycle.reconsolidate(self.store, now=now + 60)
|
|
87
|
+
self.assertEqual(result["updated"], 1)
|
|
88
|
+
row = self.read_entries()[0]
|
|
89
|
+
self.assertGreater(row.get("regenerated_at", 0), 0)
|
|
90
|
+
self.assertEqual(row.get("reconsolidation_count"), 1)
|
|
91
|
+
self.assertGreater(row.get("importance", 0), 0.5)
|
|
92
|
+
self.assertEqual(len(self.read_entries()), 1)
|
|
93
|
+
|
|
94
|
+
def test_idempotent_second_call(self):
|
|
95
|
+
now = time.time()
|
|
96
|
+
self.write_entries([entry("a", hits=1)])
|
|
97
|
+
lifecycle.record_recall(self.store, "s1", ["a"], now=now)
|
|
98
|
+
lifecycle.reconsolidate(self.store, now=now + 60)
|
|
99
|
+
again = lifecycle.reconsolidate(self.store, now=now + 61)
|
|
100
|
+
self.assertEqual(again["updated"], 0)
|
|
101
|
+
self.assertGreaterEqual(again["skipped"], 1)
|
|
102
|
+
|
|
103
|
+
def test_window_expires(self):
|
|
104
|
+
now = time.time()
|
|
105
|
+
self.write_entries([entry("a", hits=1)])
|
|
106
|
+
lifecycle.record_recall(self.store, "s1", ["a"], now=now - 7 * 3600)
|
|
107
|
+
result = lifecycle.reconsolidate(self.store, now=now)
|
|
108
|
+
self.assertEqual(result["updated"], 0)
|
|
109
|
+
self.assertEqual(result["reason"], "window")
|
|
110
|
+
|
|
111
|
+
def test_no_recall_ids(self):
|
|
112
|
+
self.write_entries([entry("a")])
|
|
113
|
+
result = lifecycle.reconsolidate(self.store, now=time.time())
|
|
114
|
+
self.assertEqual(result["updated"], 0)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class RepairTest(Case):
|
|
118
|
+
def test_arm_and_consume_once(self):
|
|
119
|
+
now = time.time()
|
|
120
|
+
self.assertTrue(lifecycle.arm_repair(self.store, "boundary", now=now))
|
|
121
|
+
lines = lifecycle.repair_lines(self.store, now=now + 1)
|
|
122
|
+
self.assertTrue(lines)
|
|
123
|
+
self.assertTrue(any("修复" in line for line in lines))
|
|
124
|
+
self.assertEqual(lifecycle.repair_lines(self.store, now=now + 2), [])
|
|
125
|
+
|
|
126
|
+
def test_cooldown_blocks_rearm(self):
|
|
127
|
+
now = time.time()
|
|
128
|
+
lifecycle.arm_repair(self.store, "boundary", now=now)
|
|
129
|
+
lifecycle.repair_lines(self.store, now=now + 1)
|
|
130
|
+
self.assertFalse(lifecycle.arm_repair(self.store, "boundary",
|
|
131
|
+
now=now + 2))
|
|
132
|
+
|
|
133
|
+
def test_expired_repair_returns_empty(self):
|
|
134
|
+
now = time.time()
|
|
135
|
+
lifecycle.arm_repair(self.store, "boundary", now=now)
|
|
136
|
+
self.assertEqual(lifecycle.repair_lines(self.store,
|
|
137
|
+
now=now + 31 * 60), [])
|
|
138
|
+
|
|
139
|
+
def test_state_exposes_lifecycle(self):
|
|
140
|
+
now = time.time()
|
|
141
|
+
self.write_entries([entry("a", hits=5)])
|
|
142
|
+
lifecycle.record_recall(self.store, "s1", ["a"], now=now)
|
|
143
|
+
lifecycle.tier_tick(self.store, [entry("a", hits=5)], now=now)
|
|
144
|
+
pipeline.dispatch("turn_end", self.payload(
|
|
145
|
+
user_text="好", assistant_text="嗯。", capture=False))
|
|
146
|
+
body = pipeline.dispatch("state", self.payload())["state"]
|
|
147
|
+
self.assertIn("tiers", body)
|
|
148
|
+
self.assertIn("reconsolidation", body)
|
|
149
|
+
self.assertIn("repair", body)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
unittest.main()
|