dsh-biocore 0.3.1 → 0.3.3
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/package.json +1 -1
- package/python/biocore_model/sanitize.py +6 -1
- package/python/biocore_model/tests/test_extensions.py +167 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1 -1
- package/python/biocore_model/tests/test_signals.py +106 -0
- package/python/biocore_runtime/extensions.py +317 -0
- package/python/biocore_runtime/pipeline.py +129 -0
- package/python/biocore_runtime/signals.py +243 -0
- package/python/data/signals.json +12247 -0
- package/ui/panel.css +123 -1
- package/ui/panel.html +2 -2
package/package.json
CHANGED
|
@@ -23,7 +23,12 @@ class ReplySanitizer:
|
|
|
23
23
|
|
|
24
24
|
CONTROL_TAG_PATTERNS = [
|
|
25
25
|
re.compile(r"<\s*(?:bc|biocore)\b[^>]*>.*?</\s*(?:bc|biocore)\s*>", re.I | re.S),
|
|
26
|
-
re.compile(r"<\s*(?:bc|biocore)\b[^>]*>[^\n]*", re.I),
|
|
26
|
+
re.compile(r"<\s*(?:bc|biocore)\b[^>]*>[^\n]*", re.I),
|
|
27
|
+
# Reminder tags are instructions for the model, never user-visible text.
|
|
28
|
+
re.compile(r"<\s*(?:system_reminder|reminder|system)\b[^>]*>.*?</\s*(?:system_reminder|reminder|system)\s*>", re.I | re.S),
|
|
29
|
+
re.compile(r"<\s*(?:system_reminder|reminder|system)\b[^>]*>[^\n]*", re.I),
|
|
30
|
+
# Models sometimes echo the instruction header verbatim.
|
|
31
|
+
re.compile(r"^\s*【[^】]*】\s*$", re.M),
|
|
27
32
|
re.compile(r"<\s*(?:thinking|think|thought|reasoning|analysis)\b[^>]*>.*?</\s*(?:thinking|think|thought|reasoning|analysis)\s*>", re.I | re.S),
|
|
28
33
|
re.compile(r"<\s*(?:thinking|think|thought|reasoning|analysis)\b[^>]*>[^\n]*", re.I),
|
|
29
34
|
re.compile(r"<\s*(?:思考|分析|推理|内心独白)\s*>.*?</\s*(?:思考|分析|推理|内心独白)\s*>", re.S),
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-225 (5b): P0 memory extensions + expression modules."""
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_model import (entity_graph, entity_resolve, expr_template,
|
|
10
|
+
graph_extract, graph_recall, layered_memory,
|
|
11
|
+
memory_dual, prompt_rules, prospective,
|
|
12
|
+
reconstruction, template_expr, topic_control,
|
|
13
|
+
turn_memory, voice_bank)
|
|
14
|
+
from biocore_runtime import extensions, pipeline
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def entry(i, text, ts=1700000000.0):
|
|
18
|
+
return {"id": i, "text": text, "role": "user", "ts": ts,
|
|
19
|
+
"importance": 0.5, "hits": 1}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Case(unittest.TestCase):
|
|
23
|
+
def setUp(self):
|
|
24
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
25
|
+
self.data_dir = Path(self.tmp.name) / "data"
|
|
26
|
+
self.store = self.data_dir / "memory"
|
|
27
|
+
(self.store / "sessions" / "s1").mkdir(parents=True)
|
|
28
|
+
|
|
29
|
+
def tearDown(self):
|
|
30
|
+
self.tmp.cleanup()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GraphTest(Case):
|
|
34
|
+
def test_entity_graph_build_search(self):
|
|
35
|
+
graph, _ = extensions._entity_graph([
|
|
36
|
+
entry("a", "苹果 很甜"), entry("b", "苹果 咖啡")])
|
|
37
|
+
self.assertGreaterEqual(len(graph.get("entities") or []), 1)
|
|
38
|
+
hits = entity_graph.search(graph, "苹果", limit=4)
|
|
39
|
+
self.assertTrue(hits)
|
|
40
|
+
|
|
41
|
+
def test_entity_resolve(self):
|
|
42
|
+
self.assertEqual(entity_resolve.normalize("苹果 甜"), "苹果甜")
|
|
43
|
+
self.assertGreaterEqual(entity_resolve.similarity("苹果", "苹果"), 0.9)
|
|
44
|
+
|
|
45
|
+
def test_graph_extract(self):
|
|
46
|
+
self.assertEqual(graph_extract.canonical_type("人物"), "person")
|
|
47
|
+
self.assertGreater(graph_extract.when_to_ts("明天", 1000.0), 1000.0)
|
|
48
|
+
|
|
49
|
+
def test_graph_recall_window(self):
|
|
50
|
+
window = graph_recall.detect_window("上周")
|
|
51
|
+
self.assertNotEqual(window, None)
|
|
52
|
+
|
|
53
|
+
def test_graph_hits_extra(self):
|
|
54
|
+
rows = [entry("a", "苹果 很甜"), entry("b", "苹果 咖啡"),
|
|
55
|
+
entry("c", "香蕉")]
|
|
56
|
+
hits, stats = extensions.graph_hits(rows, "苹果")
|
|
57
|
+
self.assertGreaterEqual(stats["hits"], 0)
|
|
58
|
+
self.assertIsInstance(hits, list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MemoryExtTest(Case):
|
|
62
|
+
def test_layered_select(self):
|
|
63
|
+
result = layered_memory.select([entry("a", "早上的事"), entry("b", "刚才的事")],
|
|
64
|
+
recalled_ids=["b"])
|
|
65
|
+
self.assertIn("resident", result)
|
|
66
|
+
self.assertIn("retrieval", result)
|
|
67
|
+
self.assertTrue(result["resident"] or result["retrieval"])
|
|
68
|
+
|
|
69
|
+
def test_memory_dual_score(self):
|
|
70
|
+
e = memory_dual.encode("苹果", 1700000000.0, arousal=0.5)
|
|
71
|
+
self.assertGreater(memory_dual.score(e, 1700000100.0), 0)
|
|
72
|
+
self.assertTrue(memory_dual.select([e], 1700000100.0, limit=2))
|
|
73
|
+
|
|
74
|
+
def test_prospective_due(self):
|
|
75
|
+
now = time.time()
|
|
76
|
+
rec = prospective.entry("his_event", "体检", now - 10)
|
|
77
|
+
items, changed = prospective.add([], rec)
|
|
78
|
+
self.assertTrue(changed)
|
|
79
|
+
due = prospective.due_now(items, now, window_s=60)
|
|
80
|
+
self.assertEqual(len(due), 1)
|
|
81
|
+
self.assertTrue(prospective.build_hint(due[0], now))
|
|
82
|
+
self.assertEqual(prospective.stats(items)["by_status"]["pending"], 1)
|
|
83
|
+
|
|
84
|
+
def test_reconstruction(self):
|
|
85
|
+
out = reconstruction.conserve([entry("a", "旧的记忆")])
|
|
86
|
+
self.assertIn("total", out)
|
|
87
|
+
|
|
88
|
+
def test_turn_memory(self):
|
|
89
|
+
rows = turn_memory.build([{"entry_id": "a", "text": "记得喝水",
|
|
90
|
+
"source": "recall"}])
|
|
91
|
+
self.assertEqual(len(rows), 1)
|
|
92
|
+
record = turn_memory.build_record(rows, reply="好", ts=10.0)
|
|
93
|
+
self.assertTrue(turn_memory.render_receipt(record))
|
|
94
|
+
|
|
95
|
+
def test_recall_graph_expansion(self):
|
|
96
|
+
rows = [entry("a", "苹果 很甜", 1700000001.0),
|
|
97
|
+
entry("b", "苹果 咖啡", 1700000002.0),
|
|
98
|
+
entry("c", "香蕉 黄", 1700000003.0)]
|
|
99
|
+
(self.store / "sessions" / "s1" / "memory.json").write_text(
|
|
100
|
+
json.dumps({"entries": rows}), encoding="utf-8")
|
|
101
|
+
out = pipeline.inject_block("s1", str(self.store), query="苹果 咖啡",
|
|
102
|
+
mode="query", top_k=3)
|
|
103
|
+
self.assertGreaterEqual(len(out["ids"]), 2)
|
|
104
|
+
self.assertIn("b", out["ids"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ExpressionTest(Case):
|
|
108
|
+
def test_expr_template(self):
|
|
109
|
+
out = expr_template.build_reply({"v": 0.2}, {"trust": 0.5})
|
|
110
|
+
self.assertIsInstance(out, dict)
|
|
111
|
+
|
|
112
|
+
def test_prompt_rules(self):
|
|
113
|
+
text = prompt_rules.render_rules(affect={"valence": -0.4, "stress": 0.7},
|
|
114
|
+
relation={"closeness": 0.5},
|
|
115
|
+
certainty=0.7, soft_target=12,
|
|
116
|
+
soft_max=30)
|
|
117
|
+
self.assertIsInstance(text, str)
|
|
118
|
+
self.assertTrue(text)
|
|
119
|
+
|
|
120
|
+
def test_template_expr(self):
|
|
121
|
+
self.assertEqual(template_expr.render("你好 {{name}}", {"name": "主管"}),
|
|
122
|
+
"你好 主管")
|
|
123
|
+
|
|
124
|
+
def test_topic_control(self):
|
|
125
|
+
result = topic_control.topic_control(turns=[], state={"v": 0.0},
|
|
126
|
+
relationship={"trust": 0.5})
|
|
127
|
+
self.assertIsInstance(result, dict)
|
|
128
|
+
self.assertTrue("tendency" in result or "holder" in result)
|
|
129
|
+
|
|
130
|
+
def test_voice_bank(self):
|
|
131
|
+
bank = [{"id": "v1", "phrase": "嗯。", "weight": 1.0,
|
|
132
|
+
"when": {"v_min": -1, "v_max": 1, "tension_min": 0,
|
|
133
|
+
"tension_max": 1}, "cooldown_s": 0}]
|
|
134
|
+
picked = voice_bank.pick({"v": 0.1, "tension": 0.2},
|
|
135
|
+
{"stage": "normal"}, [], time.time(),
|
|
136
|
+
entries=bank)
|
|
137
|
+
self.assertTrue(picked)
|
|
138
|
+
|
|
139
|
+
def test_expression_block(self):
|
|
140
|
+
block = extensions.expression_block(
|
|
141
|
+
str(self.store), {"core": {"v": -0.2, "stress": 0.7}},
|
|
142
|
+
{"trust": 0.5, "closeness": 0.4, "stage": "normal"},
|
|
143
|
+
soft_target=12, soft_max=30)
|
|
144
|
+
self.assertTrue(block["rules"] or block["template"])
|
|
145
|
+
self.assertIn("template_name", block)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class StateTest(Case):
|
|
149
|
+
def test_state_exposes_extensions(self):
|
|
150
|
+
rows = [entry("a", "苹果 很甜"), entry("b", "香蕉")]
|
|
151
|
+
(self.store / "sessions" / "s1" / "memory.json").write_text(
|
|
152
|
+
json.dumps({"entries": rows}), encoding="utf-8")
|
|
153
|
+
pipeline.dispatch("turn_end", {
|
|
154
|
+
"store": str(self.store), "session_id": "s1",
|
|
155
|
+
"user_text": "苹果很甜", "assistant_text": "嗯。",
|
|
156
|
+
"capture": False,
|
|
157
|
+
"persona_profile": str(Path(self.tmp.name) / "missing.json")})
|
|
158
|
+
body = pipeline.dispatch("state", {
|
|
159
|
+
"store": str(self.store), "session_id": "s1",
|
|
160
|
+
"persona_profile": str(Path(self.tmp.name) / "missing.json")})["state"]
|
|
161
|
+
self.assertIn("extensions", body)
|
|
162
|
+
self.assertIn("graph", body["extensions"])
|
|
163
|
+
self.assertIn("layered", body["extensions"])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
unittest.main()
|
|
@@ -1303,7 +1303,7 @@ class LengthHintTest(unittest.TestCase):
|
|
|
1303
1303
|
if "字以内" not in line and "一次只说一句" not in line]
|
|
1304
1304
|
self.assertEqual(strip(default), strip(explicit))
|
|
1305
1305
|
self.assertIn("语气自然平稳", "\n".join(default))
|
|
1306
|
-
self.assertIn("
|
|
1306
|
+
self.assertIn("先不主动发新消息", "\n".join(default))
|
|
1307
1307
|
self.assertIn("允许改口", "\n".join(default))
|
|
1308
1308
|
|
|
1309
1309
|
def test_prompt_still_has_intero_and_persona(self):
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-224 (5a): the 17 P0 modules are evaluated and exposed via /state."""
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_runtime import pipeline, signals
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SignalsCase(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
|
+
self.state = {"core": {"v": 0.2, "a": 0.6, "stress": 0.4,
|
|
19
|
+
"social": 0.4, "energy": 0.7}}
|
|
20
|
+
self.rel = {"trust": 0.55, "closeness": 0.5, "conflict": 0.1,
|
|
21
|
+
"days_known": 12}
|
|
22
|
+
self.out = signals.evaluate(self.store, "s1",
|
|
23
|
+
user_text="我们约好明天见面,谢谢你",
|
|
24
|
+
assistant_text="好。",
|
|
25
|
+
state=self.state, relationship=self.rel,
|
|
26
|
+
now=time.time())
|
|
27
|
+
|
|
28
|
+
def tearDown(self):
|
|
29
|
+
self.tmp.cleanup()
|
|
30
|
+
|
|
31
|
+
def module_ok(self, group, name):
|
|
32
|
+
row = self.out[group][name]
|
|
33
|
+
self.assertTrue(row.get("enabled"), row)
|
|
34
|
+
return row
|
|
35
|
+
|
|
36
|
+
def test_affective_labor(self):
|
|
37
|
+
self.module_ok("perception", "affective_labor")
|
|
38
|
+
|
|
39
|
+
def test_compassion_fatigue(self):
|
|
40
|
+
self.module_ok("perception", "compassion_fatigue")
|
|
41
|
+
|
|
42
|
+
def test_connection_bids(self):
|
|
43
|
+
self.module_ok("perception", "connection_bids")
|
|
44
|
+
|
|
45
|
+
def test_defense_mechanisms(self):
|
|
46
|
+
self.module_ok("perception", "defense_mechanisms")
|
|
47
|
+
|
|
48
|
+
def test_emotion_geometry(self):
|
|
49
|
+
self.module_ok("perception", "emotion_geometry")
|
|
50
|
+
|
|
51
|
+
def test_needs_events(self):
|
|
52
|
+
self.module_ok("perception", "needs_events")
|
|
53
|
+
|
|
54
|
+
def test_social_pain(self):
|
|
55
|
+
self.module_ok("perception", "social_pain")
|
|
56
|
+
|
|
57
|
+
def test_social_repair(self):
|
|
58
|
+
self.module_ok("perception", "social_repair")
|
|
59
|
+
|
|
60
|
+
def test_commitment_extractor(self):
|
|
61
|
+
row = self.module_ok("relationship", "commitment_extractor")
|
|
62
|
+
self.assertIsInstance(row.get("value"), dict)
|
|
63
|
+
|
|
64
|
+
def test_relation_stage(self):
|
|
65
|
+
self.module_ok("relationship", "relation_stage")
|
|
66
|
+
|
|
67
|
+
def test_relationship_stage(self):
|
|
68
|
+
self.module_ok("relationship", "relationship_stage")
|
|
69
|
+
|
|
70
|
+
def test_repair_after_boundary(self):
|
|
71
|
+
self.module_ok("relationship", "repair_after_boundary")
|
|
72
|
+
|
|
73
|
+
def test_metacog(self):
|
|
74
|
+
self.module_ok("self", "metacog")
|
|
75
|
+
|
|
76
|
+
def test_possible_selves(self):
|
|
77
|
+
self.module_ok("self", "possible_selves")
|
|
78
|
+
|
|
79
|
+
def test_self_interest(self):
|
|
80
|
+
self.module_ok("self", "self_interest")
|
|
81
|
+
|
|
82
|
+
def test_self_verification(self):
|
|
83
|
+
self.module_ok("self", "self_verification")
|
|
84
|
+
|
|
85
|
+
def test_counts_and_sidecar(self):
|
|
86
|
+
self.assertEqual(self.out["counts"]["perception"], 8)
|
|
87
|
+
self.assertEqual(self.out["counts"]["relationship"], 4)
|
|
88
|
+
self.assertEqual(self.out["counts"]["self"], 4)
|
|
89
|
+
doc = json.loads((self.data_dir / "signals.json").read_text(encoding="utf-8"))
|
|
90
|
+
self.assertEqual(doc["latest"]["session_id"], "s1")
|
|
91
|
+
|
|
92
|
+
def test_state_exposes_signals(self):
|
|
93
|
+
pipeline.dispatch("turn_start", {
|
|
94
|
+
"store": str(self.store), "session_id": "s1",
|
|
95
|
+
"user_text": "你好", "capture": False,
|
|
96
|
+
"persona_profile": str(Path(self.tmp.name) / "missing.json")})
|
|
97
|
+
body = pipeline.dispatch("state", {
|
|
98
|
+
"store": str(self.store), "session_id": "s1",
|
|
99
|
+
"persona_profile": str(Path(self.tmp.name) / "missing.json")})["state"]
|
|
100
|
+
self.assertIn("signals", body)
|
|
101
|
+
self.assertTrue(body["signals"]["enabled"]["metacog"])
|
|
102
|
+
self.assertEqual(body["signals"]["counts"]["self"], 4)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
unittest.main()
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-225 (5b): P0 memory extensions + expression extensions for the core.
|
|
3
|
+
|
|
4
|
+
Recall side (all optional, failure -> current lexical path):
|
|
5
|
+
entity_graph / entity_resolve / graph_extract / graph_recall lightweight
|
|
6
|
+
entity graph over memory entries; query entities pull neighbour entries
|
|
7
|
+
layered_memory resident vs retrieval tiers for the injected rows
|
|
8
|
+
memory_dual decayed dual score for ordering
|
|
9
|
+
prospective commitments in <dataDir>/prospective.json + due hints
|
|
10
|
+
reconstruction reconstruction drift summary (read-only)
|
|
11
|
+
turn_memory receipt rows for the last recall batch
|
|
12
|
+
|
|
13
|
+
Expression side (wording constraints only; never changes facts):
|
|
14
|
+
expr_template, prompt_rules, template_expr, topic_control, voice_bank
|
|
15
|
+
|
|
16
|
+
No LLM calls. All sidecar writes are atomic and bounded.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from biocore_model import (entity_graph as _graph,
|
|
25
|
+
entity_resolve as _resolve,
|
|
26
|
+
graph_extract as _extract,
|
|
27
|
+
layered_memory as _layered,
|
|
28
|
+
memory_dual as _dual,
|
|
29
|
+
prospective as _prospective,
|
|
30
|
+
reconstruction as _reconstruct,
|
|
31
|
+
turn_memory as _turn_memory,
|
|
32
|
+
expr_template as _expr_template,
|
|
33
|
+
prompt_rules as _prompt_rules,
|
|
34
|
+
template_expr as _template,
|
|
35
|
+
topic_control as _topic,
|
|
36
|
+
voice_bank as _voice)
|
|
37
|
+
|
|
38
|
+
PROSPECTIVE_FILE = "prospective.json"
|
|
39
|
+
EXTENSIONS_FILE = "extensions.json"
|
|
40
|
+
VOICE_FILE = "voice_bank.json"
|
|
41
|
+
SCHEMA = 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _root(store):
|
|
45
|
+
if isinstance(store, dict):
|
|
46
|
+
raw = store.get("root") or store.get("store") or store.get("path")
|
|
47
|
+
else:
|
|
48
|
+
raw = store
|
|
49
|
+
text = str(raw or "").strip()
|
|
50
|
+
return Path(text) if text else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _data_dir(store):
|
|
54
|
+
root = _root(store)
|
|
55
|
+
return (root.parent if root.name == "memory" else root) if root else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read(path, fallback):
|
|
59
|
+
try:
|
|
60
|
+
if path is None or not Path(path).exists():
|
|
61
|
+
return fallback
|
|
62
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
63
|
+
return data if isinstance(data, dict) else fallback
|
|
64
|
+
except Exception:
|
|
65
|
+
return fallback
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _write(path, doc):
|
|
69
|
+
try:
|
|
70
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
72
|
+
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2),
|
|
73
|
+
encoding="utf-8")
|
|
74
|
+
tmp.replace(path)
|
|
75
|
+
return True
|
|
76
|
+
except Exception:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _tokens(text):
|
|
81
|
+
import re
|
|
82
|
+
lowered = str(text or "").lower()
|
|
83
|
+
out = re.findall(r"[a-z0-9_]{3,}", lowered)
|
|
84
|
+
for run in re.findall(r"[\u4e00-\u9fff]+", lowered):
|
|
85
|
+
out.extend(run[index:index + 2] for index in range(len(run) - 1))
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _entity_graph(entries):
|
|
90
|
+
"""Tiny in-memory graph: every concept token is a node, co-occurrence an edge."""
|
|
91
|
+
graph = _graph.empty_graph()
|
|
92
|
+
names = {}
|
|
93
|
+
for entry in entries or []:
|
|
94
|
+
if not isinstance(entry, dict):
|
|
95
|
+
continue
|
|
96
|
+
tokens = list(dict.fromkeys(_tokens(entry.get("text"))))
|
|
97
|
+
for token in tokens[:12]:
|
|
98
|
+
names.setdefault(token, entry)
|
|
99
|
+
for left in tokens[:8]:
|
|
100
|
+
for right in tokens[:8]:
|
|
101
|
+
if left == right:
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
updated = _graph.upsert_edge(
|
|
105
|
+
graph, left, right, "co_occurs",
|
|
106
|
+
ts=float(entry.get("ts") or 0.0))
|
|
107
|
+
graph = updated[0] if isinstance(updated, tuple) else updated
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
for token in names:
|
|
111
|
+
try:
|
|
112
|
+
updated = _graph.upsert_entity(graph, token, "topic")
|
|
113
|
+
graph = updated[0] if isinstance(updated, tuple) else updated
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
return graph, names
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def graph_hits(entries, query, exclude=None):
|
|
120
|
+
"""Neighbour entry ids for the query's entities (graph recall)."""
|
|
121
|
+
exclude = set(exclude or [])
|
|
122
|
+
graph, by_name = _entity_graph(entries)
|
|
123
|
+
hits = []
|
|
124
|
+
seen = set()
|
|
125
|
+
try:
|
|
126
|
+
matches = _graph.search(graph, query, limit=6)
|
|
127
|
+
except Exception:
|
|
128
|
+
matches = []
|
|
129
|
+
for item in matches or []:
|
|
130
|
+
name = item.get("name") if isinstance(item, dict) else item
|
|
131
|
+
entry = by_name.get(str(name or ""))
|
|
132
|
+
entry_id = str((entry or {}).get("id") or "")
|
|
133
|
+
if entry_id and entry_id not in exclude and entry_id not in seen:
|
|
134
|
+
seen.add(entry_id)
|
|
135
|
+
hits.append(entry)
|
|
136
|
+
stats = {"entities": len(graph.get("entities") or []),
|
|
137
|
+
"edges": len(graph.get("edges") or []),
|
|
138
|
+
"hits": len(hits)}
|
|
139
|
+
try:
|
|
140
|
+
stats.update(_graph.stats(graph))
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
return hits, stats
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def expand_recall(store, entries, query, picked_ids, now=None):
|
|
147
|
+
"""Add graph neighbours / dual-score rows; never raises."""
|
|
148
|
+
stamp = float(now or time.time())
|
|
149
|
+
picked_ids = [str(item) for item in (picked_ids or [])]
|
|
150
|
+
hits, graph_stats = graph_hits(entries, query, exclude=picked_ids)
|
|
151
|
+
extra = []
|
|
152
|
+
for entry in hits[:3]:
|
|
153
|
+
if str(entry.get("id") or "") not in picked_ids:
|
|
154
|
+
extra.append(entry)
|
|
155
|
+
dual_rows = []
|
|
156
|
+
try:
|
|
157
|
+
ordered = _dual.select(entries, stamp, limit=max(4, len(picked_ids) + 2))
|
|
158
|
+
dual_rows = [entry for entry in ordered
|
|
159
|
+
if str(entry.get("id") or "") not in picked_ids]
|
|
160
|
+
except Exception:
|
|
161
|
+
dual_rows = []
|
|
162
|
+
for entry in dual_rows[:2]:
|
|
163
|
+
if entry not in extra:
|
|
164
|
+
extra.append(entry)
|
|
165
|
+
return {
|
|
166
|
+
"extra": extra,
|
|
167
|
+
"graph": graph_stats,
|
|
168
|
+
"dual": {"rows": len(dual_rows)},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def layered_view(entries, picked_ids):
|
|
173
|
+
try:
|
|
174
|
+
result = _layered.select(entries, recalled_ids=picked_ids) or {}
|
|
175
|
+
except Exception:
|
|
176
|
+
result = {}
|
|
177
|
+
resident = result.get("resident") or []
|
|
178
|
+
retrieval = result.get("retrieval") or []
|
|
179
|
+
return {"selected": len(resident) + len(retrieval),
|
|
180
|
+
"resident": len(resident), "retrieval": len(retrieval)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def prospective_view(store, now=None):
|
|
184
|
+
data_dir = _data_dir(store)
|
|
185
|
+
path = (data_dir / PROSPECTIVE_FILE) if data_dir else None
|
|
186
|
+
doc = _read(path, {"items": []})
|
|
187
|
+
items = [row for row in (doc.get("items") or []) if isinstance(row, dict)]
|
|
188
|
+
stamp = float(now or time.time())
|
|
189
|
+
due = []
|
|
190
|
+
try:
|
|
191
|
+
due = _prospective.due_now(items, stamp, window_s=3600.0)
|
|
192
|
+
except Exception:
|
|
193
|
+
due = []
|
|
194
|
+
hint = ""
|
|
195
|
+
if due:
|
|
196
|
+
try:
|
|
197
|
+
hint = _prospective.build_hint(due[0], stamp)
|
|
198
|
+
except Exception:
|
|
199
|
+
hint = ""
|
|
200
|
+
return {"pending": len([row for row in items
|
|
201
|
+
if row.get("status") in ("pending", "overdue")]),
|
|
202
|
+
"due": len(due), "hint": str(hint or "")[:80]}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def add_commitment(store, text, due_ts, now=None, kind="his_event"):
|
|
206
|
+
data_dir = _data_dir(store)
|
|
207
|
+
path = (data_dir / PROSPECTIVE_FILE) if data_dir else None
|
|
208
|
+
if path is None:
|
|
209
|
+
return None
|
|
210
|
+
doc = _read(path, {"items": []})
|
|
211
|
+
items = [row for row in (doc.get("items") or []) if isinstance(row, dict)]
|
|
212
|
+
try:
|
|
213
|
+
record = _prospective.entry(kind, text, due_ts, now=now)
|
|
214
|
+
items, changed = _prospective.add(items, record)
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
doc.update({"schema": SCHEMA, "items": items[-200:],
|
|
218
|
+
"updated_at": round(float(now or time.time()), 3)})
|
|
219
|
+
_write(path, doc)
|
|
220
|
+
return record if changed else None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def reconstruction_view(entries):
|
|
224
|
+
try:
|
|
225
|
+
result = _reconstruct.conserve(entries) or {}
|
|
226
|
+
except Exception:
|
|
227
|
+
return {"drift": 0.0}
|
|
228
|
+
return {"drift": round(float(result.get("total") or 0.0), 4),
|
|
229
|
+
"scale": result.get("scale", 1.0)}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def turn_memory_view(trace_rows, reply="", now=None):
|
|
233
|
+
try:
|
|
234
|
+
rows = _turn_memory.build(trace_rows)
|
|
235
|
+
record = _turn_memory.build_record(rows, reply=reply,
|
|
236
|
+
ts=float(now or time.time()))
|
|
237
|
+
receipt = _turn_memory.render_receipt(record)
|
|
238
|
+
except Exception:
|
|
239
|
+
rows, record, receipt = [], {}, ""
|
|
240
|
+
return {"rows": rows, "receipt": str(receipt or "")[:120],
|
|
241
|
+
"record": record}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def expression_block(store=None, state=None, relationship=None,
|
|
245
|
+
soft_target=0, soft_max=0, hour=None):
|
|
246
|
+
"""Wording constraints from the expression P0 modules."""
|
|
247
|
+
core = (state or {}).get("core") if isinstance(state, dict) else {}
|
|
248
|
+
core = core if isinstance(core, dict) else {}
|
|
249
|
+
affect = {"valence": float(core.get("v") or 0.0),
|
|
250
|
+
"arousal": float(core.get("a") or 0.5),
|
|
251
|
+
"stress": float(core.get("stress") or 0.2)}
|
|
252
|
+
rel = relationship if isinstance(relationship, dict) else {}
|
|
253
|
+
out = {"rules": "", "template": "", "topic": "", "voice": "",
|
|
254
|
+
"template_name": "default", "topic_name": "", "voice_name": ""}
|
|
255
|
+
try:
|
|
256
|
+
out["rules"] = _prompt_rules.render_rules(
|
|
257
|
+
affect=affect, relation=rel, certainty=0.7,
|
|
258
|
+
soft_target=int(soft_target or 0), soft_max=int(soft_max or 0))
|
|
259
|
+
except Exception:
|
|
260
|
+
out["rules"] = ""
|
|
261
|
+
try:
|
|
262
|
+
built = _expr_template.build_reply({}, rel)
|
|
263
|
+
out["expr_template_order"] = [str(item) for item in
|
|
264
|
+
(built.get("order") or [])][:8]
|
|
265
|
+
except Exception:
|
|
266
|
+
out["expr_template_order"] = []
|
|
267
|
+
try:
|
|
268
|
+
rendered = _template.render(
|
|
269
|
+
"{{# if tense}}稳一点说。{{/ if}}{{closing}}",
|
|
270
|
+
{"tense": float(core.get("stress") or 0.0) >= 0.6,
|
|
271
|
+
"closing": "不用急着答。"})
|
|
272
|
+
out["template"] = str(rendered or "")
|
|
273
|
+
out["template_name"] = "reply-soft"
|
|
274
|
+
except Exception:
|
|
275
|
+
out["template"] = ""
|
|
276
|
+
try:
|
|
277
|
+
topic = _topic.topic_control(turns=[], state=affect, relationship=rel,
|
|
278
|
+
hour=hour)
|
|
279
|
+
out["topic"] = str((_topic.directives(topic) or {}).get("tone") or "")
|
|
280
|
+
out["topic_name"] = str((topic or {}).get("tendency")
|
|
281
|
+
or (topic or {}).get("holder") or "")
|
|
282
|
+
except Exception:
|
|
283
|
+
out["topic"] = ""
|
|
284
|
+
try:
|
|
285
|
+
data_dir = _data_dir(store)
|
|
286
|
+
bank = _read((data_dir / VOICE_FILE) if data_dir else None,
|
|
287
|
+
{"entries": []})
|
|
288
|
+
entries = [row for row in (bank.get("entries") or [])
|
|
289
|
+
if isinstance(row, dict)]
|
|
290
|
+
picked = _voice.pick({"v": affect["valence"], "tension": affect["stress"]},
|
|
291
|
+
rel, [], float(time.time()), entries=entries)
|
|
292
|
+
if picked:
|
|
293
|
+
out["voice"] = str(picked[0].get("phrase") or "")[:40]
|
|
294
|
+
out["voice_name"] = str(picked[0].get("id") or "voice")[:24]
|
|
295
|
+
except Exception:
|
|
296
|
+
out["voice"] = ""
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def record_extension_snapshot(store, snapshot):
|
|
301
|
+
data_dir = _data_dir(store)
|
|
302
|
+
path = (data_dir / EXTENSIONS_FILE) if data_dir else None
|
|
303
|
+
if path is None:
|
|
304
|
+
return False
|
|
305
|
+
doc = _read(path, {})
|
|
306
|
+
doc.update({"schema": SCHEMA, "ts": round(float(time.time()), 3),
|
|
307
|
+
"latest": snapshot,
|
|
308
|
+
"history": ((doc.get("history") or [])
|
|
309
|
+
+ [snapshot])[-50:]})
|
|
310
|
+
return _write(path, doc)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def extension_summary(store):
|
|
314
|
+
data_dir = _data_dir(store)
|
|
315
|
+
doc = _read((data_dir / EXTENSIONS_FILE) if data_dir else None, {})
|
|
316
|
+
latest = doc.get("latest") if isinstance(doc.get("latest"), dict) else {}
|
|
317
|
+
return latest
|