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,153 +1,153 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""task-222: self layer (persona/narrative/drift) + autonomy decision/limits."""
|
|
3
|
-
import json
|
|
4
|
-
import tempfile
|
|
5
|
-
import time
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from biocore_runtime import autonomy, pipeline, self_layer
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def write_json(path, data):
|
|
13
|
-
path = Path(path)
|
|
14
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
-
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Case(unittest.TestCase):
|
|
19
|
-
def setUp(self):
|
|
20
|
-
self.tmp = tempfile.TemporaryDirectory()
|
|
21
|
-
self.data_dir = Path(self.tmp.name) / "data"
|
|
22
|
-
self.store = self.data_dir / "memory"
|
|
23
|
-
self.persona = Path(self.tmp.name) / "missing.json"
|
|
24
|
-
(self.store / "sessions").mkdir(parents=True, exist_ok=True)
|
|
25
|
-
|
|
26
|
-
def tearDown(self):
|
|
27
|
-
self.tmp.cleanup()
|
|
28
|
-
|
|
29
|
-
def payload(self, session_id="s1", **extra):
|
|
30
|
-
data = {"store": str(self.store), "session_id": session_id,
|
|
31
|
-
"persona_profile": str(self.persona)}
|
|
32
|
-
data.update(extra)
|
|
33
|
-
return data
|
|
34
|
-
|
|
35
|
-
def configure(self, **extra):
|
|
36
|
-
data = {"data_dir": str(self.data_dir)}
|
|
37
|
-
data.update(extra)
|
|
38
|
-
pipeline.dispatch("configure", data)
|
|
39
|
-
|
|
40
|
-
def actions(self, rows):
|
|
41
|
-
lines = []
|
|
42
|
-
for row in rows:
|
|
43
|
-
line = {"ts": time.time(), "kind": "write", "args": {},
|
|
44
|
-
"reason": "test", "result": {"ok": True}}
|
|
45
|
-
line.update(row)
|
|
46
|
-
lines.append(json.dumps(line, ensure_ascii=False))
|
|
47
|
-
(self.data_dir / "actions.jsonl").write_text(
|
|
48
|
-
"\n".join(lines) + "\n", encoding="utf-8")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class SelfLayerTest(Case):
|
|
52
|
-
def test_persona_locked(self):
|
|
53
|
-
self.configure(persona_text="我是安吉拉")
|
|
54
|
-
doc = self_layer.update(self.store, self_layer.hash_persona("我是安吉拉"))
|
|
55
|
-
self.assertTrue(doc["persona_locked"])
|
|
56
|
-
summary = self_layer.summary(self.store)
|
|
57
|
-
self.assertTrue(summary["persona_locked"])
|
|
58
|
-
self.assertEqual(summary["drift_count"], 0)
|
|
59
|
-
|
|
60
|
-
def test_drift_detected(self):
|
|
61
|
-
self.configure(persona_text="旧人设")
|
|
62
|
-
self_layer.update(self.store, self_layer.hash_persona("旧人设"))
|
|
63
|
-
self_layer.update(self.store, self_layer.hash_persona("新人设"))
|
|
64
|
-
summary = self_layer.summary(self.store)
|
|
65
|
-
self.assertEqual(summary["drift_count"], 1)
|
|
66
|
-
|
|
67
|
-
def test_same_hash_is_idempotent(self):
|
|
68
|
-
h = self_layer.hash_persona("稳定人设")
|
|
69
|
-
self_layer.update(self.store, h)
|
|
70
|
-
self_layer.update(self.store, h)
|
|
71
|
-
self.assertEqual(self_layer.summary(self.store)["drift_count"], 0)
|
|
72
|
-
|
|
73
|
-
def test_narrative_accumulates(self):
|
|
74
|
-
self_layer.update(self.store, "", {"kind": "warmth", "text": "谢谢你"})
|
|
75
|
-
self_layer.update(self.store, "", {"kind": "repair", "text": "对不起"})
|
|
76
|
-
summary = self_layer.summary(self.store)
|
|
77
|
-
self.assertEqual(summary["narrative_count"], 2)
|
|
78
|
-
self.assertEqual(summary["last_narrative"], "对不起")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class AutonomyTest(Case):
|
|
82
|
-
def test_default_limits(self):
|
|
83
|
-
lim = autonomy.limits(self.store)
|
|
84
|
-
self.assertEqual(lim["max_actions_per_hour"], 20)
|
|
85
|
-
self.assertEqual(lim["max_sends_per_hour"], 5)
|
|
86
|
-
self.assertEqual(lim["max_sends_per_day"], 30)
|
|
87
|
-
self.assertEqual(lim["quiet_hours"], [])
|
|
88
|
-
self.assertTrue(lim["auto_decay_on_silence"])
|
|
89
|
-
|
|
90
|
-
def test_send_when_social_low(self):
|
|
91
|
-
out = autonomy.decide(self.store, now=time.time(),
|
|
92
|
-
state={"core": {"social": 0.2, "energy": 0.7}})
|
|
93
|
-
self.assertEqual(out["action"]["kind"], "send")
|
|
94
|
-
|
|
95
|
-
def test_retrieve_when_memory_exists(self):
|
|
96
|
-
write_json(self.store / "sessions" / "autonomy" / "memory.json",
|
|
97
|
-
{"entries": [{"id": "a", "text": "hello", "ts": 1.0}]})
|
|
98
|
-
out = autonomy.decide(self.store, now=time.time(),
|
|
99
|
-
state={"core": {"social": 0.9}})
|
|
100
|
-
self.assertEqual(out["action"]["kind"], "retrieve")
|
|
101
|
-
|
|
102
|
-
def test_write_when_idle(self):
|
|
103
|
-
out = autonomy.decide(self.store, now=time.time(),
|
|
104
|
-
state={"core": {"social": 0.9, "energy": 0.8}})
|
|
105
|
-
self.assertEqual(out["action"]["kind"], "write")
|
|
106
|
-
|
|
107
|
-
def test_hourly_action_cap_blocks(self):
|
|
108
|
-
now = time.time()
|
|
109
|
-
self.actions([{"ts": now - 5}] * 20)
|
|
110
|
-
out = autonomy.decide(self.store, now=now, state={"core": {"social": 0.2}})
|
|
111
|
-
self.assertIsNone(out["action"])
|
|
112
|
-
self.assertEqual(out["reason"], "limit_actions_hour")
|
|
113
|
-
|
|
114
|
-
def test_send_hour_cap_blocks_send(self):
|
|
115
|
-
now = time.time()
|
|
116
|
-
self.actions([{"ts": now - 10, "kind": "send"}] * 5)
|
|
117
|
-
out = autonomy.decide(self.store, now=now, state={"core": {"social": 0.2}})
|
|
118
|
-
self.assertNotEqual((out["action"] or {}).get("kind"), "send")
|
|
119
|
-
|
|
120
|
-
def test_quiet_hours_block_send(self):
|
|
121
|
-
self.configure(autonomy={"quiet_hours": ["00:00-23:59"]})
|
|
122
|
-
out = autonomy.decide(self.store, now=time.time(),
|
|
123
|
-
state={"core": {"social": 0.2}})
|
|
124
|
-
self.assertNotEqual((out["action"] or {}).get("kind"), "send")
|
|
125
|
-
|
|
126
|
-
def test_decay_on_silence_throttles(self):
|
|
127
|
-
now = time.time()
|
|
128
|
-
self.actions([{"ts": now - 60}]) # last action 1 min ago
|
|
129
|
-
out = autonomy.decide(self.store, now=now,
|
|
130
|
-
state={"core": {"social": 0.2}},
|
|
131
|
-
silence_minutes=24 * 60)
|
|
132
|
-
self.assertIsNone(out["action"])
|
|
133
|
-
self.assertEqual(out["reason"], "decayed_silence")
|
|
134
|
-
|
|
135
|
-
def test_core_disabled_no_action(self):
|
|
136
|
-
self.configure(switches={"core_enabled": False})
|
|
137
|
-
out = pipeline.dispatch("autonomy_tick", self.payload())
|
|
138
|
-
self.assertTrue(out["ok"])
|
|
139
|
-
self.assertIsNone(out["action"])
|
|
140
|
-
self.assertEqual(out["reason"], "disabled")
|
|
141
|
-
|
|
142
|
-
def test_state_exposes_autonomy_numbers(self):
|
|
143
|
-
self.actions([{"ts": time.time(), "kind": "send"},
|
|
144
|
-
{"ts": time.time(), "kind": "write"}])
|
|
145
|
-
body = pipeline.dispatch("state", self.payload())["state"]
|
|
146
|
-
self.assertEqual(body["autonomy"]["actions_today"], 2)
|
|
147
|
-
self.assertEqual(body["autonomy"]["sends_today"], 1)
|
|
148
|
-
self.assertEqual(body["autonomy"]["limits"]["max_sends_per_hour"], 5)
|
|
149
|
-
self.assertIn("identity", body)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if __name__ == "__main__":
|
|
153
|
-
unittest.main()
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-222: self layer (persona/narrative/drift) + autonomy decision/limits."""
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_runtime import autonomy, pipeline, self_layer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_json(path, data):
|
|
13
|
+
path = Path(path)
|
|
14
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Case(unittest.TestCase):
|
|
19
|
+
def setUp(self):
|
|
20
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
21
|
+
self.data_dir = Path(self.tmp.name) / "data"
|
|
22
|
+
self.store = self.data_dir / "memory"
|
|
23
|
+
self.persona = Path(self.tmp.name) / "missing.json"
|
|
24
|
+
(self.store / "sessions").mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
def tearDown(self):
|
|
27
|
+
self.tmp.cleanup()
|
|
28
|
+
|
|
29
|
+
def payload(self, session_id="s1", **extra):
|
|
30
|
+
data = {"store": str(self.store), "session_id": session_id,
|
|
31
|
+
"persona_profile": str(self.persona)}
|
|
32
|
+
data.update(extra)
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
def configure(self, **extra):
|
|
36
|
+
data = {"data_dir": str(self.data_dir)}
|
|
37
|
+
data.update(extra)
|
|
38
|
+
pipeline.dispatch("configure", data)
|
|
39
|
+
|
|
40
|
+
def actions(self, rows):
|
|
41
|
+
lines = []
|
|
42
|
+
for row in rows:
|
|
43
|
+
line = {"ts": time.time(), "kind": "write", "args": {},
|
|
44
|
+
"reason": "test", "result": {"ok": True}}
|
|
45
|
+
line.update(row)
|
|
46
|
+
lines.append(json.dumps(line, ensure_ascii=False))
|
|
47
|
+
(self.data_dir / "actions.jsonl").write_text(
|
|
48
|
+
"\n".join(lines) + "\n", encoding="utf-8")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SelfLayerTest(Case):
|
|
52
|
+
def test_persona_locked(self):
|
|
53
|
+
self.configure(persona_text="我是安吉拉")
|
|
54
|
+
doc = self_layer.update(self.store, self_layer.hash_persona("我是安吉拉"))
|
|
55
|
+
self.assertTrue(doc["persona_locked"])
|
|
56
|
+
summary = self_layer.summary(self.store)
|
|
57
|
+
self.assertTrue(summary["persona_locked"])
|
|
58
|
+
self.assertEqual(summary["drift_count"], 0)
|
|
59
|
+
|
|
60
|
+
def test_drift_detected(self):
|
|
61
|
+
self.configure(persona_text="旧人设")
|
|
62
|
+
self_layer.update(self.store, self_layer.hash_persona("旧人设"))
|
|
63
|
+
self_layer.update(self.store, self_layer.hash_persona("新人设"))
|
|
64
|
+
summary = self_layer.summary(self.store)
|
|
65
|
+
self.assertEqual(summary["drift_count"], 1)
|
|
66
|
+
|
|
67
|
+
def test_same_hash_is_idempotent(self):
|
|
68
|
+
h = self_layer.hash_persona("稳定人设")
|
|
69
|
+
self_layer.update(self.store, h)
|
|
70
|
+
self_layer.update(self.store, h)
|
|
71
|
+
self.assertEqual(self_layer.summary(self.store)["drift_count"], 0)
|
|
72
|
+
|
|
73
|
+
def test_narrative_accumulates(self):
|
|
74
|
+
self_layer.update(self.store, "", {"kind": "warmth", "text": "谢谢你"})
|
|
75
|
+
self_layer.update(self.store, "", {"kind": "repair", "text": "对不起"})
|
|
76
|
+
summary = self_layer.summary(self.store)
|
|
77
|
+
self.assertEqual(summary["narrative_count"], 2)
|
|
78
|
+
self.assertEqual(summary["last_narrative"], "对不起")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AutonomyTest(Case):
|
|
82
|
+
def test_default_limits(self):
|
|
83
|
+
lim = autonomy.limits(self.store)
|
|
84
|
+
self.assertEqual(lim["max_actions_per_hour"], 20)
|
|
85
|
+
self.assertEqual(lim["max_sends_per_hour"], 5)
|
|
86
|
+
self.assertEqual(lim["max_sends_per_day"], 30)
|
|
87
|
+
self.assertEqual(lim["quiet_hours"], [])
|
|
88
|
+
self.assertTrue(lim["auto_decay_on_silence"])
|
|
89
|
+
|
|
90
|
+
def test_send_when_social_low(self):
|
|
91
|
+
out = autonomy.decide(self.store, now=time.time(),
|
|
92
|
+
state={"core": {"social": 0.2, "energy": 0.7}})
|
|
93
|
+
self.assertEqual(out["action"]["kind"], "send")
|
|
94
|
+
|
|
95
|
+
def test_retrieve_when_memory_exists(self):
|
|
96
|
+
write_json(self.store / "sessions" / "autonomy" / "memory.json",
|
|
97
|
+
{"entries": [{"id": "a", "text": "hello", "ts": 1.0}]})
|
|
98
|
+
out = autonomy.decide(self.store, now=time.time(),
|
|
99
|
+
state={"core": {"social": 0.9}})
|
|
100
|
+
self.assertEqual(out["action"]["kind"], "retrieve")
|
|
101
|
+
|
|
102
|
+
def test_write_when_idle(self):
|
|
103
|
+
out = autonomy.decide(self.store, now=time.time(),
|
|
104
|
+
state={"core": {"social": 0.9, "energy": 0.8}})
|
|
105
|
+
self.assertEqual(out["action"]["kind"], "write")
|
|
106
|
+
|
|
107
|
+
def test_hourly_action_cap_blocks(self):
|
|
108
|
+
now = time.time()
|
|
109
|
+
self.actions([{"ts": now - 5}] * 20)
|
|
110
|
+
out = autonomy.decide(self.store, now=now, state={"core": {"social": 0.2}})
|
|
111
|
+
self.assertIsNone(out["action"])
|
|
112
|
+
self.assertEqual(out["reason"], "limit_actions_hour")
|
|
113
|
+
|
|
114
|
+
def test_send_hour_cap_blocks_send(self):
|
|
115
|
+
now = time.time()
|
|
116
|
+
self.actions([{"ts": now - 10, "kind": "send"}] * 5)
|
|
117
|
+
out = autonomy.decide(self.store, now=now, state={"core": {"social": 0.2}})
|
|
118
|
+
self.assertNotEqual((out["action"] or {}).get("kind"), "send")
|
|
119
|
+
|
|
120
|
+
def test_quiet_hours_block_send(self):
|
|
121
|
+
self.configure(autonomy={"quiet_hours": ["00:00-23:59"]})
|
|
122
|
+
out = autonomy.decide(self.store, now=time.time(),
|
|
123
|
+
state={"core": {"social": 0.2}})
|
|
124
|
+
self.assertNotEqual((out["action"] or {}).get("kind"), "send")
|
|
125
|
+
|
|
126
|
+
def test_decay_on_silence_throttles(self):
|
|
127
|
+
now = time.time()
|
|
128
|
+
self.actions([{"ts": now - 60}]) # last action 1 min ago
|
|
129
|
+
out = autonomy.decide(self.store, now=now,
|
|
130
|
+
state={"core": {"social": 0.2}},
|
|
131
|
+
silence_minutes=24 * 60)
|
|
132
|
+
self.assertIsNone(out["action"])
|
|
133
|
+
self.assertEqual(out["reason"], "decayed_silence")
|
|
134
|
+
|
|
135
|
+
def test_core_disabled_no_action(self):
|
|
136
|
+
self.configure(switches={"core_enabled": False})
|
|
137
|
+
out = pipeline.dispatch("autonomy_tick", self.payload())
|
|
138
|
+
self.assertTrue(out["ok"])
|
|
139
|
+
self.assertIsNone(out["action"])
|
|
140
|
+
self.assertEqual(out["reason"], "disabled")
|
|
141
|
+
|
|
142
|
+
def test_state_exposes_autonomy_numbers(self):
|
|
143
|
+
self.actions([{"ts": time.time(), "kind": "send"},
|
|
144
|
+
{"ts": time.time(), "kind": "write"}])
|
|
145
|
+
body = pipeline.dispatch("state", self.payload())["state"]
|
|
146
|
+
self.assertEqual(body["autonomy"]["actions_today"], 2)
|
|
147
|
+
self.assertEqual(body["autonomy"]["sends_today"], 1)
|
|
148
|
+
self.assertEqual(body["autonomy"]["limits"]["max_sends_per_hour"], 5)
|
|
149
|
+
self.assertIn("identity", body)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
unittest.main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Contract migration 1: cache stats + series are computed by the core."""
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_runtime import cache_stats, core_state, pipeline
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheCase(unittest.TestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
15
|
+
self.data_dir = Path(self.tmp.name) / "data"
|
|
16
|
+
self.store = self.data_dir / "memory"
|
|
17
|
+
(self.store / "sessions").mkdir(parents=True)
|
|
18
|
+
|
|
19
|
+
def tearDown(self):
|
|
20
|
+
self.tmp.cleanup()
|
|
21
|
+
|
|
22
|
+
def event(self, **values):
|
|
23
|
+
row = {"prompt_tokens": 100, "output_tokens": 20,
|
|
24
|
+
"cache_read": 60, "cache_write": 40}
|
|
25
|
+
row.update(values)
|
|
26
|
+
return cache_stats.record_event(self.store, row,
|
|
27
|
+
now=values.pop("ts", time.time()))
|
|
28
|
+
|
|
29
|
+
def test_accumulates_events(self):
|
|
30
|
+
now = time.time()
|
|
31
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 10,
|
|
32
|
+
"output_tokens": 2, "cache_read": 4, "cache_write": 1}, now=now)
|
|
33
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 20,
|
|
34
|
+
"output_tokens": 3, "cache_read": 6, "cache_write": 2}, now=now)
|
|
35
|
+
out = cache_stats.compute(self.store, now=now)
|
|
36
|
+
self.assertEqual(out["turns"], 2)
|
|
37
|
+
self.assertEqual(out["prompt_tokens"], 30)
|
|
38
|
+
self.assertEqual(out["output_tokens"], 5)
|
|
39
|
+
self.assertEqual(out["cache_read"], 10)
|
|
40
|
+
self.assertEqual(out["cache_write"], 3)
|
|
41
|
+
self.assertEqual(out["source"], "usage_events")
|
|
42
|
+
|
|
43
|
+
def test_hit_rate_ratio(self):
|
|
44
|
+
now = time.time()
|
|
45
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 100,
|
|
46
|
+
"output_tokens": 10, "cache_read": 75, "cache_write": 25}, now=now)
|
|
47
|
+
self.assertAlmostEqual(cache_stats.compute(self.store, now=now)["hit_rate"],
|
|
48
|
+
0.75, places=4)
|
|
49
|
+
|
|
50
|
+
def test_hit_rate_falls_back_to_prompt_tokens(self):
|
|
51
|
+
now = time.time()
|
|
52
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 100,
|
|
53
|
+
"output_tokens": 10, "cache_read": 50, "cache_write": 0}, now=now)
|
|
54
|
+
self.assertAlmostEqual(cache_stats.compute(self.store, now=now)["hit_rate"],
|
|
55
|
+
0.5, places=4)
|
|
56
|
+
|
|
57
|
+
def test_hit_rate_zero_when_empty(self):
|
|
58
|
+
self.assertEqual(cache_stats.compute(self.store)["hit_rate"], 0.0)
|
|
59
|
+
|
|
60
|
+
def test_hit_rate_is_clamped(self):
|
|
61
|
+
now = time.time()
|
|
62
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 10,
|
|
63
|
+
"output_tokens": 1, "cache_read": 50, "cache_write": 0}, now=now)
|
|
64
|
+
rate = cache_stats.compute(self.store, now=now)["hit_rate"]
|
|
65
|
+
self.assertGreaterEqual(rate, 0.0)
|
|
66
|
+
self.assertLessEqual(rate, 1.0)
|
|
67
|
+
|
|
68
|
+
def test_window_excludes_old_events(self):
|
|
69
|
+
now = time.time()
|
|
70
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 10,
|
|
71
|
+
"cache_read": 5}, now=now - 8 * 86400)
|
|
72
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 20,
|
|
73
|
+
"cache_read": 10}, now=now)
|
|
74
|
+
out = cache_stats.compute(self.store, now=now)
|
|
75
|
+
self.assertEqual(out["turns"], 1)
|
|
76
|
+
self.assertEqual(out["prompt_tokens"], 20)
|
|
77
|
+
|
|
78
|
+
def test_legacy_file_is_read_only_fallback(self):
|
|
79
|
+
now = time.time()
|
|
80
|
+
(self.data_dir / "cache_stats.json").write_text(json.dumps({
|
|
81
|
+
"turns": 4, "prompt_tokens": 400, "output_tokens": 40,
|
|
82
|
+
"cache_read": 200, "cache_write": 200, "hit_rate": 0.5,
|
|
83
|
+
"updated_at": now}), encoding="utf-8")
|
|
84
|
+
out = cache_stats.compute(self.store, now=now)
|
|
85
|
+
self.assertEqual(out["source"], "legacy_file")
|
|
86
|
+
self.assertEqual(out["turns"], 4)
|
|
87
|
+
self.assertEqual(out["hit_rate"], 0.5)
|
|
88
|
+
|
|
89
|
+
def test_usage_event_action_appends(self):
|
|
90
|
+
out = pipeline.dispatch("usage_event", {
|
|
91
|
+
"store": str(self.store), "prompt_tokens": 30,
|
|
92
|
+
"cache_read": 15, "cache_write": 5, "now": time.time()})
|
|
93
|
+
self.assertTrue(out["ok"])
|
|
94
|
+
self.assertEqual(out["cache"]["cache_read"], 15)
|
|
95
|
+
|
|
96
|
+
def test_stats_action_includes_core_cache(self):
|
|
97
|
+
cache_stats.record_event(self.store, {"prompt_tokens": 10,
|
|
98
|
+
"cache_read": 5, "cache_write": 5}, now=time.time())
|
|
99
|
+
out = pipeline.dispatch("stats", {"store": str(self.store)})
|
|
100
|
+
self.assertIn("cache", out)
|
|
101
|
+
self.assertEqual(out["cache"]["source"], "usage_events")
|
|
102
|
+
self.assertEqual(out["cache"]["hit_rate"], 0.5)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SeriesCase(unittest.TestCase):
|
|
106
|
+
def setUp(self):
|
|
107
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
108
|
+
self.data_dir = Path(self.tmp.name) / "data"
|
|
109
|
+
self.store = self.data_dir / "memory"
|
|
110
|
+
(self.store / "sessions").mkdir(parents=True)
|
|
111
|
+
|
|
112
|
+
def tearDown(self):
|
|
113
|
+
self.tmp.cleanup()
|
|
114
|
+
|
|
115
|
+
def test_empty_series_arrays(self):
|
|
116
|
+
out = pipeline.dispatch("series", {"store": str(self.store),
|
|
117
|
+
"data_dir": str(self.data_dir)})
|
|
118
|
+
self.assertTrue(out["ok"])
|
|
119
|
+
for key in ("ts", "v", "tension", "warmth", "heaviness", "clarity"):
|
|
120
|
+
self.assertIsInstance(out["series"][key], list)
|
|
121
|
+
self.assertEqual(out["series"][key], [])
|
|
122
|
+
|
|
123
|
+
def test_series_points_from_core_state(self):
|
|
124
|
+
path = core_state.path_for(data_dir=str(self.data_dir))
|
|
125
|
+
now = time.time()
|
|
126
|
+
core_state.apply(path, absolute={"v": 0.2, "stress": 0.4}, now=now)
|
|
127
|
+
core_state.apply(path, absolute={"v": 0.3, "stress": 0.2}, now=now + 60)
|
|
128
|
+
out = pipeline.dispatch("series", {"store": str(self.store),
|
|
129
|
+
"data_dir": str(self.data_dir),
|
|
130
|
+
"limit": 24})
|
|
131
|
+
self.assertTrue(out["points"])
|
|
132
|
+
self.assertLessEqual(len(out["points"]), 24)
|
|
133
|
+
self.assertEqual(len(out["series"]["ts"]), len(out["points"]))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
unittest.main()
|