dsh-biocore 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/lib/index.js +170 -27
  2. package/package.json +1 -1
  3. package/python/biocore_runtime/pipeline.py +42 -0
  4. package/python/biocore_runtime/rerank.py +201 -0
  5. package/python/mindbase/__init__.py +18 -0
  6. package/python/mindbase/__main__.py +3 -0
  7. package/python/mindbase/actions.py +56 -0
  8. package/python/mindbase/active_inference.py +54 -0
  9. package/python/mindbase/addressing.py +69 -0
  10. package/python/mindbase/affect.py +86 -0
  11. package/python/mindbase/affective.py +111 -0
  12. package/python/mindbase/allostasis.py +28 -0
  13. package/python/mindbase/appraisal.py +101 -0
  14. package/python/mindbase/attachment.py +49 -0
  15. package/python/mindbase/attention.py +30 -0
  16. package/python/mindbase/autonomy.py +81 -0
  17. package/python/mindbase/autonomy_bench.py +251 -0
  18. package/python/mindbase/autonomy_evolve.py +342 -0
  19. package/python/mindbase/autonomy_loop.py +65 -0
  20. package/python/mindbase/autonomy_meta.py +91 -0
  21. package/python/mindbase/base_only.py +28 -0
  22. package/python/mindbase/benchmark.py +61 -0
  23. package/python/mindbase/bio_adaptation.py +274 -0
  24. package/python/mindbase/bio_noise.py +39 -0
  25. package/python/mindbase/bio_semantic.py +138 -0
  26. package/python/mindbase/bm25.py +46 -0
  27. package/python/mindbase/bootstrap.py +256 -0
  28. package/python/mindbase/bootstrap_real.py +231 -0
  29. package/python/mindbase/brain.py +178 -0
  30. package/python/mindbase/care.py +78 -0
  31. package/python/mindbase/chaos_search.py +79 -0
  32. package/python/mindbase/character_profile.py +33 -0
  33. package/python/mindbase/circadian.py +38 -0
  34. package/python/mindbase/circuit.py +235 -0
  35. package/python/mindbase/clock.py +53 -0
  36. package/python/mindbase/companion_runtime.py +366 -0
  37. package/python/mindbase/companion_train.py +126 -0
  38. package/python/mindbase/config.py +66 -0
  39. package/python/mindbase/connection_state.py +76 -0
  40. package/python/mindbase/connectome_prior.py +35 -0
  41. package/python/mindbase/connectome_world.py +158 -0
  42. package/python/mindbase/constructs.py +88 -0
  43. package/python/mindbase/context_builder.py +58 -0
  44. package/python/mindbase/critical.py +36 -0
  45. package/python/mindbase/criticality.py +123 -0
  46. package/python/mindbase/defaults.py +27 -0
  47. package/python/mindbase/demo.py +163 -0
  48. package/python/mindbase/diary.py +116 -0
  49. package/python/mindbase/diary_service.py +40 -0
  50. package/python/mindbase/digital_prior.py +37 -0
  51. package/python/mindbase/diversity.py +110 -0
  52. package/python/mindbase/drives.py +106 -0
  53. package/python/mindbase/embedding.py +132 -0
  54. package/python/mindbase/embedding_memory.py +74 -0
  55. package/python/mindbase/embodiment.py +83 -0
  56. package/python/mindbase/emotion_calibration.py +39 -0
  57. package/python/mindbase/emotion_calibrator.py +50 -0
  58. package/python/mindbase/emotion_core.py +113 -0
  59. package/python/mindbase/emotion_dynamics.py +104 -0
  60. package/python/mindbase/emotion_intent.py +164 -0
  61. package/python/mindbase/emotion_layer.py +200 -0
  62. package/python/mindbase/emotion_schema.py +92 -0
  63. package/python/mindbase/engine.py +781 -0
  64. package/python/mindbase/episode_log.py +90 -0
  65. package/python/mindbase/evaluation.py +493 -0
  66. package/python/mindbase/event_appraisal.py +84 -0
  67. package/python/mindbase/evolution.py +77 -0
  68. package/python/mindbase/evolve_offline.py +232 -0
  69. package/python/mindbase/experiments.py +358 -0
  70. package/python/mindbase/expression.py +54 -0
  71. package/python/mindbase/external_brain.py +157 -0
  72. package/python/mindbase/fly_mb.py +159 -0
  73. package/python/mindbase/fly_tasks.py +45 -0
  74. package/python/mindbase/forage.py +179 -0
  75. package/python/mindbase/goal_generator.py +98 -0
  76. package/python/mindbase/goals.py +153 -0
  77. package/python/mindbase/grid_world.py +123 -0
  78. package/python/mindbase/group_autonomy.py +92 -0
  79. package/python/mindbase/group_context.py +67 -0
  80. package/python/mindbase/homeostatic.py +71 -0
  81. package/python/mindbase/homeostatic_action.py +38 -0
  82. package/python/mindbase/human_standard.py +53 -0
  83. package/python/mindbase/identity_suite.py +50 -0
  84. package/python/mindbase/image_context.py +27 -0
  85. package/python/mindbase/impression.py +119 -0
  86. package/python/mindbase/initiative_quota.py +145 -0
  87. package/python/mindbase/insight.py +51 -0
  88. package/python/mindbase/intelligence_search.py +108 -0
  89. package/python/mindbase/intention.py +76 -0
  90. package/python/mindbase/interaction_log.py +30 -0
  91. package/python/mindbase/interoception.py +67 -0
  92. package/python/mindbase/invariants.py +53 -0
  93. package/python/mindbase/iteration.py +29 -0
  94. package/python/mindbase/layered_memory.py +170 -0
  95. package/python/mindbase/layers/__init__.py +18 -0
  96. package/python/mindbase/layers/l0_substrate.py +39 -0
  97. package/python/mindbase/layers/l1_homeostasis.py +6 -0
  98. package/python/mindbase/layers/l2_affect.py +6 -0
  99. package/python/mindbase/layers/l3_attention.py +6 -0
  100. package/python/mindbase/layers/l4_learning.py +17 -0
  101. package/python/mindbase/layers/l5_self_social.py +35 -0
  102. package/python/mindbase/layers/l6_expression.py +23 -0
  103. package/python/mindbase/layers/l7_evaluation.py +45 -0
  104. package/python/mindbase/learner.py +196 -0
  105. package/python/mindbase/lexicon.py +33 -0
  106. package/python/mindbase/life_state.py +169 -0
  107. package/python/mindbase/lifecycle.py +87 -0
  108. package/python/mindbase/liquid_readout.py +48 -0
  109. package/python/mindbase/llm_adapter.py +233 -0
  110. package/python/mindbase/llm_cache.py +77 -0
  111. package/python/mindbase/llm_gate.py +85 -0
  112. package/python/mindbase/llm_openai.py +130 -0
  113. package/python/mindbase/llm_setup.py +99 -0
  114. package/python/mindbase/longrun.py +83 -0
  115. package/python/mindbase/longterm_memory.py +606 -0
  116. package/python/mindbase/mb/__init__.py +14 -0
  117. package/python/mindbase/mb/apl.py +22 -0
  118. package/python/mindbase/mb/dan.py +14 -0
  119. package/python/mindbase/mb/habituation.py +27 -0
  120. package/python/mindbase/mb/kc.py +28 -0
  121. package/python/mindbase/mb/mbon.py +23 -0
  122. package/python/mindbase/mb/mushroom_body.py +116 -0
  123. package/python/mindbase/memory.py +201 -0
  124. package/python/mindbase/memory_framework.py +158 -0
  125. package/python/mindbase/memory_retriever.py +110 -0
  126. package/python/mindbase/memory_summarizer.py +177 -0
  127. package/python/mindbase/memory_task.py +32 -0
  128. package/python/mindbase/memory_trace.py +46 -0
  129. package/python/mindbase/meta.py +32 -0
  130. package/python/mindbase/motivation.py +31 -0
  131. package/python/mindbase/multi_session.py +19 -0
  132. package/python/mindbase/narrative.py +87 -0
  133. package/python/mindbase/needs.py +77 -0
  134. package/python/mindbase/neural.py +45 -0
  135. package/python/mindbase/neuron_models.py +134 -0
  136. package/python/mindbase/noise.py +309 -0
  137. package/python/mindbase/one_on_one.py +60 -0
  138. package/python/mindbase/online_iteration.py +63 -0
  139. package/python/mindbase/ostracism.py +35 -0
  140. package/python/mindbase/parallel_eval.py +60 -0
  141. package/python/mindbase/parallel_qd.py +64 -0
  142. package/python/mindbase/persona.py +221 -0
  143. package/python/mindbase/persona_ab.py +64 -0
  144. package/python/mindbase/persona_bridge.py +129 -0
  145. package/python/mindbase/persona_compile.py +34 -0
  146. package/python/mindbase/persona_dsh.py +120 -0
  147. package/python/mindbase/persona_learner.py +157 -0
  148. package/python/mindbase/physiology.py +197 -0
  149. package/python/mindbase/population_lifecycle.py +83 -0
  150. package/python/mindbase/preferences.py +50 -0
  151. package/python/mindbase/proactive_agent.py +274 -0
  152. package/python/mindbase/proactive_gate.py +53 -0
  153. package/python/mindbase/proactive_group.py +119 -0
  154. package/python/mindbase/proactive_policy.py +422 -0
  155. package/python/mindbase/proactive_profile.py +196 -0
  156. package/python/mindbase/proactive_timing.py +28 -0
  157. package/python/mindbase/pyramid_stages.py +61 -0
  158. package/python/mindbase/quantum.py +132 -0
  159. package/python/mindbase/quantum_chaos.py +98 -0
  160. package/python/mindbase/reflection.py +68 -0
  161. package/python/mindbase/regulation_posture.py +50 -0
  162. package/python/mindbase/relationship_session.py +431 -0
  163. package/python/mindbase/relationship_suite.py +45 -0
  164. package/python/mindbase/relationships.py +70 -0
  165. package/python/mindbase/replay.py +22 -0
  166. package/python/mindbase/reply_diversity.py +53 -0
  167. package/python/mindbase/reply_gate.py +52 -0
  168. package/python/mindbase/rerank.py +41 -0
  169. package/python/mindbase/rerank_local.py +46 -0
  170. package/python/mindbase/ripple.py +187 -0
  171. package/python/mindbase/robust_search.py +68 -0
  172. package/python/mindbase/runtime.py +113 -0
  173. package/python/mindbase/scoped_identity.py +116 -0
  174. package/python/mindbase/self_insight.py +51 -0
  175. package/python/mindbase/self_model.py +116 -0
  176. package/python/mindbase/selftest.py +118 -0
  177. package/python/mindbase/semantic_memory.py +108 -0
  178. package/python/mindbase/session_store.py +175 -0
  179. package/python/mindbase/session_window.py +31 -0
  180. package/python/mindbase/shadow_eval.py +42 -0
  181. package/python/mindbase/sim_env.py +86 -0
  182. package/python/mindbase/social.py +138 -0
  183. package/python/mindbase/social_constraint.py +141 -0
  184. package/python/mindbase/social_feedback.py +95 -0
  185. package/python/mindbase/social_norm.py +225 -0
  186. package/python/mindbase/spiking_reservoir.py +204 -0
  187. package/python/mindbase/spontaneity.py +40 -0
  188. package/python/mindbase/story_loop.py +203 -0
  189. package/python/mindbase/survival_gauntlet.py +128 -0
  190. package/python/mindbase/symbolic.py +170 -0
  191. package/python/mindbase/teacher.py +244 -0
  192. package/python/mindbase/theory.py +42 -0
  193. package/python/mindbase/theory_of_mind.py +98 -0
  194. package/python/mindbase/translation_layer.py +80 -0
  195. package/python/mindbase/turn_eval.py +52 -0
  196. package/python/mindbase/types.py +78 -0
  197. package/python/mindbase/uncertainty_field.py +61 -0
  198. package/python/mindbase/user_loop.py +45 -0
  199. package/python/mindbase/user_profile.py +73 -0
  200. package/python/mindbase/value_model.py +64 -0
  201. package/python/mindbase/values.py +102 -0
  202. package/python/mindbase/vector_store.py +78 -0
  203. package/python/mindbase/viability.py +41 -0
  204. package/python/mindbase/virtual_body.py +90 -0
  205. package/python/mindbase/will.py +60 -0
  206. package/python/mindbase/working_memory.py +32 -0
  207. package/python/mindbase/workspace.py +55 -0
  208. package/ui/panel.css +16 -5
  209. package/ui/panel.html +25 -1
  210. package/ui/panel.js +57 -15
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import random
5
+ from typing import Dict, List
6
+
7
+ from .config import MindConfig
8
+ from .noise import clip, ou
9
+
10
+
11
+ class PopulationClock:
12
+ """Multi-scale leaky integrators (2h/12h/48h) + circadian + sleep pressure."""
13
+
14
+ def __init__(self, cfg: MindConfig, rng: random.Random):
15
+ self.cfg = cfg
16
+ self.rng = rng
17
+ self.scales: List[float] = [0.0] * len(cfg.clock_taus)
18
+ self.sleep_pressure = 0.25
19
+ self.t = 0.0
20
+ self.phase = 0.0
21
+
22
+ def tick(self, dt: float, activity: float = 0.0, asleep: bool = False) -> None:
23
+ self.t += dt
24
+ for i, tau in enumerate(self.cfg.clock_taus):
25
+ theta = 1.0 / max(tau, 1e-6)
26
+ self.scales[i] = ou(self.scales[i], activity, theta, 0.02, dt, self.rng)
27
+ rate = 0.000004 * dt
28
+ if asleep:
29
+ self.sleep_pressure = clip(self.sleep_pressure - rate * 2.0, 0.0, 1.0)
30
+ else:
31
+ self.sleep_pressure = clip(self.sleep_pressure + rate, 0.0, 1.0)
32
+
33
+ @property
34
+ def drive(self) -> float:
35
+ return sum(w * s for w, s in zip(self.cfg.clock_weights, self.scales))
36
+
37
+ @property
38
+ def circadian(self) -> float:
39
+ # 24h cycle; 0.5 at t=0, low at night. Demo uses sim time.
40
+ return 0.5 + 0.5 * math.sin(2.0 * math.pi * (self.t / 86400.0 - 0.25 + self.phase))
41
+
42
+ @property
43
+ def asleep(self) -> bool:
44
+ return self.circadian < 0.22 and self.sleep_pressure > 0.65
45
+
46
+ def snapshot(self) -> Dict[str, float]:
47
+ return {
48
+ "drive": round(self.drive, 4),
49
+ "circadian": round(self.circadian, 4),
50
+ "sleep_pressure": round(self.sleep_pressure, 4),
51
+ "asleep": self.asleep,
52
+ "scales": [round(x, 4) for x in self.scales],
53
+ }
@@ -0,0 +1,366 @@
1
+ """Persistent companion runtime: heartbeat, memory books, affect, learning.
2
+
3
+ The model is only one tool; the character lives in the loop outside it.
4
+ Borrow the housecat runtime shell, but replace its rules with biological state.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Callable, Dict, List, Optional, Sequence
13
+
14
+ from .affective import perceive_emotion_lexicon
15
+ from .noise import clip
16
+
17
+ ROOT = Path("mindbase/data/companion_runtime")
18
+
19
+
20
+ class MemoryStore:
21
+ """Diary / impressions / profiles / learning / blacklist / audit."""
22
+
23
+ def __init__(self, root: Path | str = ROOT):
24
+ self.root = Path(root)
25
+ self.root.mkdir(parents=True, exist_ok=True)
26
+ for name in ("diary", "impressions", "profiles", "learning", "blacklist"):
27
+ path = self.root / f"{name}.json"
28
+ if not path.exists():
29
+ path.write_text(json.dumps([] if name == "diary" else {}, ensure_ascii=False), encoding="utf-8")
30
+
31
+ def _load(self, name: str):
32
+ try:
33
+ return json.loads((self.root / f"{name}.json").read_text(encoding="utf-8"))
34
+ except Exception:
35
+ return [] if name == "diary" else {}
36
+
37
+ def _save(self, name: str, data) -> None:
38
+ (self.root / f"{name}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
39
+
40
+ def append_diary(self, text: str, tags: Sequence[str] = ()) -> None:
41
+ rows = self._load("diary")
42
+ rows.append({"t": time.time(), "text": str(text), "tags": list(tags)})
43
+ self._save("diary", rows[-500:])
44
+
45
+ def upsert_impression(self, user_id: str, text: str) -> None:
46
+ data = self._load("impressions")
47
+ data[str(user_id)] = {"text": str(text)[:1200], "t": time.time()}
48
+ self._save("impressions", data)
49
+
50
+ def upsert_profile(self, user_id: str, **fields) -> None:
51
+ data = self._load("profiles")
52
+ data.setdefault(str(user_id), {}).update(fields)
53
+ self._save("profiles", data)
54
+
55
+ def learn(self, text: str, reward: Optional[float] = None) -> None:
56
+ data = self._load("learning")
57
+ data[str(int(time.time() * 1000))] = {"text": str(text)[:400], "reward": reward}
58
+ self._save("learning", data)
59
+
60
+ def blacklist(self, text: str) -> None:
61
+ data = self._load("blacklist")
62
+ data[str(int(time.time() * 1000))] = str(text)[:400]
63
+ self._save("blacklist", data)
64
+
65
+ def recall(self, user_id: str, limit: int = 5) -> str:
66
+ parts: List[str] = []
67
+ learning = self._load("learning")
68
+ if learning:
69
+ rows = list(learning.values())[-3:]
70
+ texts = [str(r.get("text", r)) if isinstance(r, dict) else str(r) for r in rows]
71
+ parts.append("她学到的:" + " / ".join(texts))
72
+ imp = (self._load("impressions") or {}).get(str(user_id))
73
+ if imp:
74
+ parts.append("她对这个人的印象:" + str(imp.get("text", ""))[:300])
75
+ prof = (self._load("profiles") or {}).get(str(user_id))
76
+ if prof:
77
+ parts.append("这个人的资料:" + json.dumps(prof, ensure_ascii=False)[:300])
78
+ diary = self._load("diary")
79
+ if diary:
80
+ rows = []
81
+ for r in diary[-limit:]:
82
+ t = str(r.get("text", ""))
83
+ if t.startswith("用户说:"):
84
+ t = t[len("用户说:"):]
85
+ if ";她回:" in t:
86
+ t = t.split(";她回:", 1)[0]
87
+ rows.append(t[:100])
88
+ parts.append("最近的日记(只取用户说过的):" + " / ".join(rows))
89
+ return "\n".join(parts)
90
+
91
+ def consolidate(self) -> None:
92
+ rows = self._load("diary")
93
+ self._save("diary", rows[-200:])
94
+ learning = self._load("learning")
95
+ items = list(learning.items())[-200:]
96
+ self._save("learning", dict(items))
97
+
98
+ def audit(self, event: Dict[str, object]) -> None:
99
+ with (self.root / "audit.jsonl").open("a", encoding="utf-8") as fh:
100
+ fh.write(json.dumps({"t": time.time(), **event}, ensure_ascii=False) + "\n")
101
+
102
+
103
+ def default_responder(text: str, emotion: str, affect: Dict[str, object], recall: str) -> List[str]:
104
+ v = float(affect.get("v", 0.0))
105
+ if emotion in ("sadness", "anxiety", "loneliness", "anger"):
106
+ head = "我在。" if v < 0.2 else "我听着呢。"
107
+ return [head, "你慢慢说,不用一个人扛。"]
108
+ if emotion == "joy":
109
+ return ["听起来真好。", "我也替你开心。"]
110
+ return ["我在。", "你继续说。"]
111
+
112
+
113
+ @dataclass
114
+ class RuntimeConfig:
115
+ cooldown: float = 1.0
116
+ max_chars: int = 1200
117
+ proactive_quiet_s: float = 3600.0
118
+ proactive_cooldown: float = 7200.0
119
+ token_budget: float = 1.0
120
+
121
+
122
+ class HomeostaticClock:
123
+ """Heartbeat interval from energy/stress, not a fixed cron."""
124
+
125
+ def __init__(self, base_interval: float = 1.0):
126
+ self.base_interval = max(0.05, float(base_interval))
127
+
128
+ def interval(self, mind) -> float:
129
+ try:
130
+ energy = float(mind.drives.values.get("energy", 0.5))
131
+ stress = float(mind.affect.stress)
132
+ except Exception:
133
+ energy, stress = 0.5, 0.0
134
+ return clip(self.base_interval * (1.0 + 1.5 * stress + 1.0 * (1.0 - energy)), 0.2, 10.0)
135
+
136
+
137
+ class MetabolicBudget:
138
+ """Token budget as metabolism: low energy shortens/denies replies."""
139
+
140
+ def __init__(self, energy: float = 1.0):
141
+ self.energy = clip(float(energy), 0.0, 1.0)
142
+
143
+ def tick(self, dt: float) -> None:
144
+ self.energy = clip(self.energy + 0.03 * float(dt) - 0.005, 0.0, 1.0)
145
+
146
+ def can_respond(self) -> bool:
147
+ return self.energy > 0.10
148
+
149
+ def spend(self, cost: float = 0.02) -> None:
150
+ self.energy = clip(self.energy - float(cost), 0.0, 1.0)
151
+
152
+ def length_scale(self) -> float:
153
+ return clip(0.6 + 1.4 * self.energy, 0.6, 2.0)
154
+
155
+
156
+ class FeedbackLearner:
157
+ """Updates empathy gain and proactive threshold from real feedback."""
158
+
159
+ def __init__(self, path: Optional[Path | str] = None, lr: float = 0.05):
160
+ self.path = Path(path) if path else None
161
+ self.lr = float(lr)
162
+ self.empathy_gain = 0.35
163
+ self.proactive_threshold = 0.65
164
+ self.count = 0
165
+ self._load()
166
+
167
+ def _load(self) -> None:
168
+ if not self.path or not self.path.exists():
169
+ return
170
+ try:
171
+ data = json.loads(self.path.read_text(encoding="utf-8"))
172
+ self.empathy_gain = float(data.get("empathy_gain", self.empathy_gain))
173
+ self.proactive_threshold = float(data.get("proactive_threshold", self.proactive_threshold))
174
+ self.count = int(data.get("count", 0))
175
+ except Exception:
176
+ pass
177
+
178
+ def save(self) -> None:
179
+ if not self.path:
180
+ return
181
+ self.path.parent.mkdir(parents=True, exist_ok=True)
182
+ self.path.write_text(json.dumps({
183
+ "empathy_gain": round(self.empathy_gain, 6),
184
+ "proactive_threshold": round(self.proactive_threshold, 6),
185
+ "count": self.count,
186
+ }, ensure_ascii=False, indent=1), encoding="utf-8")
187
+
188
+ def update(self, reward: float, proactive: bool = False) -> Dict[str, float]:
189
+ r = clip(float(reward), 0.0, 1.0)
190
+ err = r - 0.5
191
+ self.empathy_gain = clip(self.empathy_gain + self.lr * err, 0.05, 0.85)
192
+ if proactive:
193
+ self.proactive_threshold = clip(self.proactive_threshold - self.lr * err, 0.35, 0.95)
194
+ self.count += 1
195
+ self.save()
196
+ return {"empathy_gain": round(self.empathy_gain, 4),
197
+ "proactive_threshold": round(self.proactive_threshold, 4)}
198
+
199
+
200
+ class HeartbeatRuntime:
201
+ def __init__(self, mind, store: Optional[MemoryStore] = None,
202
+ responder: Optional[Callable] = None,
203
+ sender: Optional[Callable[[List[str]], None]] = None,
204
+ cfg: Optional[RuntimeConfig] = None,
205
+ learner: Optional[FeedbackLearner] = None,
206
+ budget: Optional[MetabolicBudget] = None,
207
+ perceiver: Optional[Callable] = None,
208
+ perceiver_affect: Optional[Callable] = None):
209
+ self.mind = mind
210
+ self.store = store or MemoryStore()
211
+ self.responder = responder or default_responder
212
+ self.sender = sender or (lambda lines: None)
213
+ self.cfg = cfg or RuntimeConfig()
214
+ self.learner = learner or FeedbackLearner()
215
+ self.budget = budget or MetabolicBudget()
216
+ self.clock = HomeostaticClock()
217
+ self.perceiver = perceiver or perceive_emotion_lexicon
218
+ self.perceiver_affect = perceiver_affect
219
+ self.pending: List[Dict[str, str]] = []
220
+ self.last_emotion: str = "neutral"
221
+ self.last_user_id: str = "default"
222
+ self.proactive_sent = 0
223
+ self.proactive_feedback = 0
224
+ self.proactive_accepted = 0
225
+ self.last_sent = -1e9
226
+ self.last_user = -1e9
227
+ self.last_proactive = -1e9
228
+ self.last_state: Dict[str, object] = {}
229
+
230
+ def receive(self, text: str, user_id: str = "default") -> bool:
231
+ t = str(text or "").strip()
232
+ black = set(str(x) for x in (self.store._load("blacklist") or {}).values())
233
+ if not t or t in black:
234
+ return False
235
+ self.pending.append({"text": t, "user_id": str(user_id)})
236
+ self.last_user = time.time()
237
+ return True
238
+
239
+ def tick(self, now: Optional[float] = None) -> Optional[Dict[str, object]]:
240
+ now = time.time() if now is None else float(now)
241
+ dt = 1.0 if self.last_sent < -1e8 else max(0.0, now - self.last_sent)
242
+ self.budget.tick(dt)
243
+ if not self.pending or now - self.last_sent < self.cfg.cooldown:
244
+ return None
245
+ if not self.budget.can_respond():
246
+ self.store.audit({"kind": "budget_block", "energy": round(self.budget.energy, 3)})
247
+ return None
248
+ merged = " ".join(p["text"] for p in self.pending)[-self.cfg.max_chars:]
249
+ user_id = self.pending[-1]["user_id"]
250
+ self.pending.clear()
251
+ try:
252
+ from .types import Event
253
+ self.mind.step(Event(text=merged, speaker=user_id, directed=True,
254
+ ts=self.mind.t + 60))
255
+ except Exception:
256
+ pass
257
+ read = None
258
+ if self.perceiver_affect is not None:
259
+ try:
260
+ read = self.perceiver_affect(merged)
261
+ self.mind.inject_affect(read.get("valence", 0.0), read.get("arousal", 0.5),
262
+ read.get("dominance", 0.0), read.get("intensity", 0.7))
263
+ from .emotion_schema import emotion_readout, top_emotions
264
+ ro = emotion_readout(read.get("valence", 0.0), read.get("arousal", 0.5),
265
+ read.get("dominance", 0.0))
266
+ top = top_emotions(ro, 1)
267
+ emotion = str(top[0][0]) if top else "neutral"
268
+ intensity = float(read.get("intensity") or 0.5)
269
+ except Exception:
270
+ read = None
271
+ if read is None:
272
+ emotion, intensity = self.perceiver(merged)
273
+ self.mind.empathize(emotion, intensity, empathy_gain=self.learner.empathy_gain)
274
+ self.last_emotion = str(emotion)
275
+ self.last_user_id = str(user_id)
276
+ recall = self.store.recall(user_id)
277
+ snap = self.mind.snapshot()
278
+ lines = self.responder(merged, emotion, snap.get("affect", {}), recall)
279
+ lines = [str(x) for x in lines if str(x).strip()][:6]
280
+ self.sender(lines)
281
+ self.budget.spend()
282
+ self.store.append_diary(f"用户说:{merged[:400]};她回:{' / '.join(lines)[:400]}", tags=[emotion])
283
+ self.store.audit({"kind": "tick", "emotion": emotion, "intensity": intensity,
284
+ "reply": lines, "empathy_gain": self.learner.empathy_gain,
285
+ "energy": round(self.budget.energy, 3)})
286
+ self.last_sent = now
287
+ self.last_state = {"emotion": emotion, "reply": lines, "recall": recall,
288
+ "empathy_gain": self.learner.empathy_gain,
289
+ "affect": read or {}}
290
+ return self.last_state
291
+
292
+ def feedback(self, reward: float, text: str = "", user_id: str = "default",
293
+ proactive: bool = False) -> Dict[str, float]:
294
+ if proactive:
295
+ self.proactive_feedback += 1
296
+ if float(reward) >= 0.5:
297
+ self.proactive_accepted += 1
298
+ try:
299
+ self.mind.post_reply("", feedback=float(reward),
300
+ speaker=self.last_user_id or "web", responded=True)
301
+ except Exception:
302
+ pass
303
+ out = self.learner.update(reward, proactive=proactive)
304
+ self.store.learn(f"反馈 {float(reward):.2f}: {str(text)[:200]}", reward=float(reward))
305
+ self.store.audit({"kind": "feedback", "reward": float(reward), "proactive": proactive, **out})
306
+ return out
307
+
308
+ def proactive(self, now: Optional[float] = None, force: bool = False) -> Optional[List[str]]:
309
+ now = time.time() if now is None else float(now)
310
+ if self.pending:
311
+ return None
312
+ if not force and now - self.last_user < self.cfg.proactive_quiet_s:
313
+ return None
314
+ if not force and now - self.last_proactive < self.cfg.proactive_cooldown:
315
+ return None
316
+ if not self.budget.can_respond():
317
+ return None
318
+ try:
319
+ social = float(self.mind.drives.values.get("social", 0.5))
320
+ except Exception:
321
+ social = 0.5
322
+ if social < self.learner.proactive_threshold:
323
+ return None
324
+ recall = self.store.recall(self.last_user_id)
325
+ if self.last_emotion in ("sadness", "anxiety", "loneliness", "fear"):
326
+ lines = ["刚才你说有点难受,", "现在好一点了吗?我在。"]
327
+ elif self.last_emotion == "anger":
328
+ lines = ["刚才那件事,", "你还在生气吗?我在这儿。"]
329
+ elif recall:
330
+ lines = ["在吗?", "突然想起你之前说的,想问问你现在怎么样了。"]
331
+ else:
332
+ lines = ["在吗?", "突然想跟你说句话。"]
333
+ self.sender(lines)
334
+ self.budget.spend()
335
+ self.last_proactive = now
336
+ self.proactive_sent += 1
337
+ self.store.audit({"kind": "proactive", "social": social, "reply": lines,
338
+ "threshold": self.learner.proactive_threshold,
339
+ "emotion": self.last_emotion, "recall": recall[:200]})
340
+ return lines
341
+
342
+ def send_generated(self, reply: str) -> None:
343
+ text = str(reply or "").strip()
344
+ if not text:
345
+ return
346
+ self.sender([text])
347
+ self.budget.spend()
348
+ self.last_proactive = time.time()
349
+ self.proactive_sent += 1
350
+ self.store.audit({"kind": "proactive_generated", "reply": text,
351
+ "emotion": self.last_emotion})
352
+
353
+ def health(self) -> Dict[str, object]:
354
+ try:
355
+ energy = float(self.mind.drives.values.get("energy", 0.5))
356
+ stress = float(self.mind.affect.stress)
357
+ except Exception:
358
+ energy, stress = 0.5, 0.0
359
+ return {"energy": round(energy, 3), "stress": round(stress, 3),
360
+ "budget": round(self.budget.energy, 3),
361
+ "interval": round(self.clock.interval(self.mind), 3),
362
+ "empathy_gain": round(self.learner.empathy_gain, 3),
363
+ "proactive_threshold": round(self.learner.proactive_threshold, 3),
364
+ "proactive_sent": self.proactive_sent,
365
+ "proactive_feedback": self.proactive_feedback,
366
+ "proactive_accepted": self.proactive_accepted}
@@ -0,0 +1,126 @@
1
+ """DSH-orchestrated companion training loop.
2
+
3
+ DSH is the teacher/orchestrator. The thing being trained is a small affect
4
+ policy (empathy gain), not the LLM weights. A real LLM judge can replace the
5
+ scripted teacher later.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import random
11
+ from dataclasses import dataclass
12
+ from typing import Dict, List
13
+
14
+ from .config import MindConfig
15
+ from .engine import MindBase
16
+
17
+
18
+ USER_EMOTIONS = ("sadness", "anxiety", "loneliness", "joy", "anger", "calm")
19
+ NEGATIVE = frozenset(("sadness", "anxiety", "loneliness", "anger"))
20
+
21
+
22
+ @dataclass
23
+ class Episode:
24
+ emotion: str
25
+ intensity: float
26
+ gain: float
27
+ response: str
28
+ reward: float
29
+ state: Dict[str, float]
30
+
31
+
32
+ class ScriptedTeacher:
33
+ """Cheap teacher used until a real DSH/LLM judge is attached."""
34
+
35
+ RESPONSES = {
36
+ "sadness": ("我在,不用一个人扛。", "听起来真的很难受,我陪你一会儿。"),
37
+ "anxiety": ("先别急,我们一件一件来。", "我在听,你慢慢说。"),
38
+ "loneliness": ("我在呢,你不是一个人。", "想说话的时候我都在。"),
39
+ "joy": ("听起来真好,我也替你开心。", "这个值得记下来。"),
40
+ "anger": ("这确实让人生气,我先听你说。", "不用憋着,我在。"),
41
+ "calm": ("这样挺好,慢慢来。", "我在,陪你待一会儿。"),
42
+ }
43
+
44
+ def respond(self, emotion: str, rng: random.Random) -> str:
45
+ return rng.choice(self.RESPONSES.get(emotion, ("我在。",)))
46
+
47
+ def judge(self, emotion: str, response: str, state: Dict[str, float]) -> float:
48
+ score = 0.0
49
+ if emotion in NEGATIVE:
50
+ if state.get("v", 0.0) <= 0.15:
51
+ score += 0.35
52
+ if state.get("stress", 0.0) >= 0.2:
53
+ score += 0.20
54
+ if any(w in response for w in ("我在", "陪", "听", "不用")):
55
+ score += 0.30
56
+ else:
57
+ if state.get("v", 0.0) >= -0.1:
58
+ score += 0.40
59
+ if any(w in response for w in ("开心", "好", "记")):
60
+ score += 0.30
61
+ # too much mirroring is not always good
62
+ if state.get("v", 0.0) < -0.9 or state.get("stress", 0.0) > 0.9:
63
+ score -= 0.20
64
+ return max(0.0, min(1.0, round(score, 4)))
65
+
66
+
67
+ class EmpathyBandit:
68
+ def __init__(self, arms=(0.15, 0.30, 0.45, 0.60, 0.75), epsilon: float = 0.25, rng=None):
69
+ self.arms = tuple(float(x) for x in arms)
70
+ self.epsilon = float(epsilon)
71
+ self.rng = rng or random.Random(1)
72
+ self.q = {a: 0.0 for a in self.arms}
73
+ self.n = {a: 0 for a in self.arms}
74
+
75
+ def choose(self) -> float:
76
+ if self.rng.random() < self.epsilon:
77
+ return self.rng.choice(self.arms)
78
+ return max(self.arms, key=lambda a: (self.q[a], -self.n[a]))
79
+
80
+ def update(self, arm: float, reward: float) -> None:
81
+ self.n[arm] += 1
82
+ self.q[arm] += (reward - self.q[arm]) / self.n[arm]
83
+
84
+ def best(self) -> float:
85
+ return max(self.arms, key=lambda a: self.q[a])
86
+
87
+
88
+ class DSHCompanionTrainer:
89
+ def __init__(self, seed: int = 1, teacher=None):
90
+ self.rng = random.Random(int(seed))
91
+ self.teacher = teacher or ScriptedTeacher()
92
+ self.bandit = EmpathyBandit(rng=self.rng)
93
+
94
+ def run_episode(self, emotion: str, intensity: float, gain: float) -> Episode:
95
+ mb = MindBase(MindConfig(seed=self.rng.randint(1, 10**6)))
96
+ mb.empathize(emotion, intensity, empathy_gain=gain)
97
+ response = self.teacher.respond(emotion, self.rng)
98
+ snap = mb.snapshot()
99
+ reward = self.teacher.judge(emotion, response, snap.get("affect", {}))
100
+ return Episode(emotion, intensity, gain, response, reward, snap.get("affect", {}))
101
+
102
+ def train(self, episodes: int = 60) -> Dict[str, object]:
103
+ rows: List[Episode] = []
104
+ for _ in range(int(episodes)):
105
+ emotion = self.rng.choice(USER_EMOTIONS)
106
+ intensity = round(self.rng.uniform(0.4, 0.9), 3)
107
+ arm = self.bandit.choose()
108
+ ep = self.run_episode(emotion, intensity, arm)
109
+ self.bandit.update(arm, ep.reward)
110
+ rows.append(ep)
111
+ best = self.bandit.best()
112
+ return {
113
+ "episodes": len(rows),
114
+ "best_gain": best,
115
+ "q": {str(k): round(v, 4) for k, v in self.bandit.q.items()},
116
+ "n": {str(k): int(v) for k, v in self.bandit.n.items()},
117
+ "mean_reward": round(sum(r.reward for r in rows) / max(1, len(rows)), 4),
118
+ "best_reward": round(max(r.reward for r in rows), 4),
119
+ "last": rows[-5:],
120
+ }
121
+
122
+
123
+ def save_result(result: Dict[str, object], path: str = "mindbase/data/dsh_companion_train.json") -> str:
124
+ with open(path, "w", encoding="utf-8") as fh:
125
+ json.dump(result, fh, ensure_ascii=False, indent=1, default=str)
126
+ return path
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, Optional, Tuple
5
+
6
+
7
+ @dataclass
8
+ class MindConfig:
9
+ """All tunable parameters of the base. Units are seconds unless noted."""
10
+
11
+ seed: int = 42
12
+ dt: float = 60.0
13
+
14
+ # memory
15
+ memory_capacity: int = 128
16
+ memory_half_life: float = 30.0 * 86400.0 # 30 days, paper: power-law-ish decay
17
+ memory_min_importance: float = 0.05
18
+ # recency / importance / frequency / relevance
19
+ memory_activation_weights: Tuple[float, float, float, float] = (0.35, 0.25, 0.20, 0.20)
20
+ memory_retrieval: str = "bm25" # bm25 | activation
21
+ memory_affect_weight: float = 0.0 # 0 = off; affect-congruent retrieval bonus
22
+
23
+ # working memory
24
+ wm_capacity: int = 5
25
+ wm_decay_tau: float = 300.0
26
+
27
+ # affect / mood
28
+ affect_tau: float = 20.0
29
+ mood_tau: float = 900.0
30
+ stress_tau: float = 300.0
31
+ sleep_recovery_tau: float = 4 * 3600.0
32
+ sleep_stress_floor: float = 0.05
33
+ noise_sigma: float = 0.03
34
+
35
+ # population clock: 2h / 12h / 48h
36
+ clock_taus: Tuple[float, ...] = (2 * 3600.0, 12 * 3600.0, 48 * 3600.0)
37
+ clock_weights: Tuple[float, ...] = (0.5, 0.3, 0.2)
38
+ sleep_pressure_tau: float = 16 * 3600.0
39
+
40
+ # autonomy / spontaneous action
41
+ autonomy_tau: float = 900.0
42
+ autonomy_threshold: float = 0.9
43
+ autonomy_viability_target: float = 0.90
44
+ autonomy_social_target: float = 0.50
45
+ autonomy_activity_target: float = 0.30
46
+ autonomy_adapt_lr: float = 0.004
47
+ volition_temperature: float = 0.0 # 0 = deterministic; >0 = stochastic will
48
+ energy_policy_threshold: float = 0.20
49
+ uncertainty_gain: float = 0.0
50
+ chaos_r: float = 3.9
51
+ autonomy_cooldown: float = 180.0
52
+
53
+ # meta / abstention
54
+ abstain_threshold: float = 0.35
55
+
56
+ # social
57
+ social_tau: float = 600.0
58
+ contagion_strength: float = 0.15
59
+
60
+ # optional persona profile: Big Five + physiology/biology numbers
61
+ persona_profile: Optional[Dict[str, float]] = None
62
+
63
+ # trained heads (offline Appraisal / Policy / World)
64
+ appraisal_blend: float = 0.35
65
+ policy_prior_threshold: float = 0.55
66
+ world_margin: float = 0.10
@@ -0,0 +1,76 @@
1
+ """Continuous social connection state, inspired by Jiwen (积温).
2
+
3
+ Five axes are reduced to three here; valence/arousal are read from affect.
4
+ No LLM call; deterministic drift and thresholds.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ def _clip(x, lo=0.0, hi=1.0):
12
+ try:
13
+ return max(lo, min(hi, float(x)))
14
+ except Exception:
15
+ return lo
16
+
17
+
18
+ @dataclass
19
+ class ConnectionState:
20
+ connection: float = 0.0
21
+ pride: float = 0.35
22
+ immersion: float = 0.0
23
+ rate_mult: float = 1.0
24
+ force_threshold: float = 0.50
25
+ last_tick: float = 0.0
26
+ last_user: float = 0.0
27
+
28
+ def note_user(self, now: float) -> None:
29
+ self.connection = _clip(self.connection * 0.55 - 0.05)
30
+ self.pride = _clip(self.pride - 0.06, -1.0, 1.0)
31
+ self.last_user = float(now)
32
+
33
+ def tick(self, now: float, valence: float = 0.0, stress: float = 0.0) -> None:
34
+ now = float(now)
35
+ if self.last_tick <= 0:
36
+ self.last_tick = now
37
+ dt_min = min(180.0, max(0.0, (now - self.last_tick) / 60.0))
38
+ if dt_min <= 0:
39
+ return
40
+ silence_min = max(0.0, (now - self.last_user) / 60.0) if self.last_user else dt_min
41
+ rate = 0.0025 * max(0.1, min(3.0, float(self.rate_mult)))
42
+ if silence_min > 30:
43
+ rate *= 1.0 + min(2.0, (silence_min - 30.0) / 60.0)
44
+ if valence < -0.2:
45
+ rate *= 0.75
46
+ if stress > 0.7:
47
+ rate *= 0.8
48
+ self.connection = _clip(self.connection + rate * dt_min)
49
+ if silence_min > 10:
50
+ self.pride = _clip(self.pride + 0.0008 * dt_min, -1.0, 1.0)
51
+ else:
52
+ self.pride = _clip(self.pride - 0.0008 * dt_min, -1.0, 1.0)
53
+ self.immersion = _clip(self.immersion - 0.01 * dt_min)
54
+ self.last_tick = now
55
+
56
+ def trigger(self) -> str:
57
+ c = self.connection
58
+ force = max(0.25, min(0.85, float(self.force_threshold)))
59
+ if c >= force:
60
+ return "contact_force"
61
+ if c >= 0.35:
62
+ return "find_activity" if self.pride >= 0.5 else "contact"
63
+ if c >= 0.20:
64
+ return "observation"
65
+ return "idle"
66
+
67
+ def style_context(self) -> str:
68
+ c = self.connection
69
+ p = self.pride
70
+ if c < 0.20:
71
+ return ""
72
+ if c >= 0.50:
73
+ return "社交连接=%.2f 骄傲=%.2f 沉浸=%.2f;已经憋不住想找人,别再硬撑。" % (c, p, self.immersion)
74
+ if p >= 0.5:
75
+ return "社交连接=%.2f 骄傲=%.2f 沉浸=%.2f;想找但嘴硬,可以找点事做,别直接说想。" % (c, p, self.immersion)
76
+ return "社交连接=%.2f 骄傲=%.2f 沉浸=%.2f;有点想说话,但不一定非要现在开口。" % (c, p, self.immersion)