dsh-biocore 0.2.0 → 0.3.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.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,104 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Step-3 dispatcher text tests: prompt / fallback / punctuation cleanup.
3
+
4
+ The two methods live on BioCorePlugin in main.py, which cannot be imported
5
+ without AstrBot. They are pure (no astrbot names inside), so the test extracts
6
+ them from the main.py AST and binds them to a fake plugin object -- the same
7
+ technique as the step-3 engine smoke harness.
8
+ """
9
+ import ast
10
+ import re
11
+ import sys
12
+ import unittest
13
+ from pathlib import Path
14
+
15
+ PLUG = Path(__file__).resolve().parents[2]
16
+ if str(PLUG) not in sys.path:
17
+ sys.path.insert(0, str(PLUG))
18
+ import interpretation # noqa: E402
19
+
20
+
21
+ class _Log:
22
+ def __getattr__(self, _name):
23
+ return lambda *a, **k: None
24
+
25
+
26
+ def load_methods():
27
+ tree = ast.parse((PLUG / "main.py").read_text(encoding="utf-8"))
28
+ cls = next(n for n in tree.body
29
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
30
+ names = ("_build_proactive_text", "_sanitize_proactive_text")
31
+ nodes = [f for f in cls.body if isinstance(f, ast.FunctionDef) and f.name in names]
32
+ if len(nodes) != 2:
33
+ raise AssertionError("expected 2 methods, got %d" % len(nodes))
34
+ ns = {"re": re, "interpretation": interpretation, "logger": _Log()}
35
+ for node in nodes:
36
+ exec(compile(ast.Module(body=[node], type_ignores=[]),
37
+ "<main-extract>", "exec"), ns)
38
+ _sanitize_fn = ns["_sanitize_proactive_text"]
39
+ return (ns["_build_proactive_text"],
40
+ lambda value, max_chars=0: _sanitize_fn(None, value, max_chars))
41
+
42
+
43
+ class _Core:
44
+ relationship_context = {"state": {"closeness": 0.4, "trust": 0.55}}
45
+
46
+ def __init__(self, parts):
47
+ self._parts = parts
48
+
49
+ def autonomy_state(self, relationship=None, now=None, persist=False):
50
+ return {"enabled": True, "max_chars": 80, "parts": dict(self._parts)}
51
+
52
+ def snapshot(self):
53
+ return {"labels": {"emotion": "loneliness", "need": "talk"},
54
+ "v": -0.2, "a": 0.4, "d": 0.5, "social": 0.4,
55
+ "stress": 0.3, "energy": 0.5, "parts": dict(self._parts)}
56
+
57
+
58
+ class _Plugin:
59
+ persona_text = "你是小灰,短句。"
60
+
61
+ def _relationship_of(self, user_data):
62
+ return {"state": {"closeness": 0.4, "trust": 0.55}}
63
+
64
+ def _autonomy_template(self, core):
65
+ return "在。"
66
+
67
+
68
+ class AutonomyModuleTextTest(unittest.TestCase):
69
+ @classmethod
70
+ def setUpClass(cls):
71
+ _build, _sanitize = load_methods()
72
+ cls.build = staticmethod(_build)
73
+ cls.sanitize = staticmethod(_sanitize)
74
+ cls.plugin = _Plugin()
75
+ cls.now = 1761700000.0
76
+ cls.acc = {"last_user_msg_ts": cls.now - 2 * 3600.0}
77
+
78
+ def test_prompt_non_empty(self):
79
+ built = self.build(self.plugin, _Core({"social_gap": 0.6, "lonely": 0.3}),
80
+ self.acc, self.now)
81
+ self.assertTrue(str(built.get("prompt") or "").strip())
82
+ self.assertNotIn("【输出格式", built["prompt"])
83
+ self.assertTrue(str(built.get("text") or "").strip())
84
+ # 2026-10-31: hard cap removed by product decision (no truncation).
85
+ self.assertEqual(int(built.get("max_chars") or 0), 0)
86
+
87
+ def test_fallback_path(self):
88
+ built = self.build(self.plugin, _Core({}), self.acc, self.now)
89
+ text = str(built.get("text") or "")
90
+ self.assertTrue(text.startswith("("))
91
+ self.assertTrue(text.endswith("在。"))
92
+ self.assertIn("你是小灰", built["prompt"]) # persona/system block present
93
+
94
+ def test_sanitize_punctuation_and_cap(self):
95
+ self.assertEqual(self.sanitize("想听听你今天的碎片。。 。", 0),
96
+ "想听听你今天的碎片。")
97
+ self.assertEqual(self.sanitize('你好<bc>{"e":"joy"}</bc>。。', 0), "你好。")
98
+ capped = self.sanitize("一二三四五六。", 3)
99
+ self.assertLessEqual(len(capped), 3)
100
+ self.assertEqual(self.sanitize(" 在。。。 ", 0), "在。")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ unittest.main()
@@ -0,0 +1,163 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Persona-derived autonomy rhythm (theta/tau/cooldown) tests."""
3
+ import ast
4
+ import unittest
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+
8
+ import interpretation
9
+ from biocore_model import autonomy_model as am
10
+ from biocore_model import autonomy_params as ap
11
+
12
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
13
+ SOURCE = MAIN.read_text(encoding="utf-8")
14
+ NOW = 1_000_000.0
15
+ REAL = {"O": 0.82, "C": 0.38, "E": 0.93, "A": 0.70, "N": 0.52,
16
+ "recovery_rate": 0.78, "social_baseline": 0.88}
17
+ REL = {"state": {"closeness": 0.5, "conflict": 0.3, "repair": 0.2},
18
+ "attachment": {"safe": 0.6}, "attachment_style": "secure"}
19
+
20
+
21
+ class CalmCore:
22
+ v = 0.0
23
+ a = 0.45
24
+ d = 0.55
25
+ social = 0.6
26
+ stress = 0.1
27
+ energy = 0.5
28
+ allostatic_load = 0.0
29
+ boredom = 0.0
30
+ homeostasis = {"L_int": 0.05}
31
+ labels = {"emotion": "neutral", "intensity": 0.0}
32
+ open_loops = []
33
+ open_loops_due = 0
34
+
35
+ def snapshot(self):
36
+ return {"v": self.v, "a": self.a, "d": self.d, "social": self.social,
37
+ "stress": self.stress, "energy": self.energy,
38
+ "allostatic_load": self.allostatic_load, "boredom": self.boredom,
39
+ "homeostasis": dict(self.homeostasis),
40
+ "open_loops": [], "open_loops_due": 0}
41
+
42
+
43
+ class MonotonicTest(unittest.TestCase):
44
+ def test_extraversion_up_theta_down(self):
45
+ low = ap.from_profile(dict(REAL, E=0.2), REL)
46
+ high = ap.from_profile(dict(REAL, E=0.95), REL)
47
+ self.assertLess(high["theta"], low["theta"])
48
+
49
+ def test_closeness_up_theta_and_tau_down(self):
50
+ far = {"state": {"closeness": 0.2}}
51
+ near = {"state": {"closeness": 0.9}}
52
+ a = ap.from_profile(REAL, far)
53
+ b = ap.from_profile(REAL, near)
54
+ self.assertLess(b["theta"], a["theta"])
55
+ self.assertLess(b["tau_hours"], a["tau_hours"])
56
+
57
+ def test_avoidance_up_theta_and_cooldown_up(self):
58
+ secure = ap.from_profile(REAL, {"attachment_style": "secure"})
59
+ avoid = ap.from_profile(REAL, {"attachment_style": "avoidant"})
60
+ self.assertGreater(avoid["theta"], secure["theta"])
61
+ self.assertGreater(avoid["cooldown_s"], secure["cooldown_s"])
62
+
63
+ def test_ignore_rate_up_cooldown_up(self):
64
+ calm = ap.from_profile(REAL, REL, ignore_rate=0.0)
65
+ ignored = ap.from_profile(REAL, REL, ignore_rate=0.9)
66
+ self.assertGreater(ignored["cooldown_s"], calm["cooldown_s"])
67
+
68
+
69
+ class RealProfileTest(unittest.TestCase):
70
+ def test_real_profile_values(self):
71
+ params = ap.from_profile(REAL, REL, responsiveness=0.5, ignore_rate=0.0)
72
+ self.assertAlmostEqual(params["theta"], 0.5239, places=3)
73
+ self.assertAlmostEqual(params["tau_hours"], 16.0, places=1)
74
+ self.assertEqual(params["cooldown_s"], 1625)
75
+ self.assertAlmostEqual(params["silence_tau_hours"], 2.001, places=2)
76
+ self.assertEqual(params["daily_cap"], 3)
77
+
78
+ def test_flat_and_nested_profile_agree(self):
79
+ flat = ap.from_profile(REAL, REL)
80
+ nested = ap.from_profile({"big5": {"E": 0.93, "N": 0.52}}, REL)
81
+ self.assertAlmostEqual(flat["theta"], nested["theta"], places=2)
82
+
83
+ def test_override_applies_and_clamps(self):
84
+ base = ap.from_profile(REAL, REL)
85
+ out = ap.apply_overrides(base, {"theta": 0.9, "daily_cap": 99,
86
+ "cooldown_s": 5})
87
+ self.assertAlmostEqual(out["theta"], 0.9, places=3)
88
+ self.assertLessEqual(out["daily_cap"], 12)
89
+ self.assertGreaterEqual(out["cooldown_s"], 300)
90
+
91
+
92
+ class CadenceTest(unittest.TestCase):
93
+ def _trigger_minutes(self):
94
+ params = ap.from_profile(REAL, REL)
95
+ acc = {"last_user_msg_ts": NOW}
96
+ value = 0.0
97
+ for minute in range(0, 300):
98
+ parts = am.parts_from_state(
99
+ CalmCore(), REL, now=NOW + minute * 60.0, acc=acc,
100
+ params=params, social_baseline=REAL["social_baseline"])
101
+ out = am.step(value, am.drift_value(parts), leak=0.15,
102
+ sigma=0.0, theta=params["theta"])
103
+ value = out["V"]
104
+ if out["fire"]:
105
+ return minute
106
+ return None
107
+
108
+ def test_real_profile_cadence_60_to_120_minutes(self):
109
+ minutes = self._trigger_minutes()
110
+ self.assertIsNotNone(minutes)
111
+ self.assertGreaterEqual(minutes, 60)
112
+ self.assertLessEqual(minutes, 120)
113
+
114
+ def test_params_change_parts_taus(self):
115
+ fast = ap.from_profile(REAL, REL)
116
+ slow = dict(fast, silence_tau_hours=8.0, social_tau_hours=36.0)
117
+ acc = {"last_user_msg_ts": NOW - 2 * 3600}
118
+ a = am.parts_from_state(CalmCore(), REL, now=NOW, acc=acc, params=fast,
119
+ social_baseline=REAL["social_baseline"])
120
+ b = am.parts_from_state(CalmCore(), REL, now=NOW, acc=acc, params=slow,
121
+ social_baseline=REAL["social_baseline"])
122
+ self.assertGreater(a["lonely"], b["lonely"])
123
+ self.assertGreater(a["social_gap"], b["social_gap"])
124
+
125
+
126
+ class Big5FlatReadTest(unittest.TestCase):
127
+ def _load_big5(self):
128
+ tree = ast.parse(SOURCE)
129
+ cls = next(n for n in tree.body
130
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
131
+ node = next(f for f in cls.body
132
+ if isinstance(f, ast.FunctionDef) and f.name == "_big5_effect")
133
+ ns = {"interpretation": interpretation}
134
+ clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
135
+ decorator_list=[], returns=node.returns,
136
+ type_comment=None, lineno=node.lineno,
137
+ col_offset=node.col_offset,
138
+ end_lineno=node.end_lineno,
139
+ end_col_offset=node.end_col_offset)
140
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<big5>", "exec"), ns)
141
+ return ns[node.name]
142
+
143
+ def test_flat_ocean_keys_are_read(self):
144
+ fn = self._load_big5()
145
+ core = SimpleNamespace(profile=dict(REAL), a=0.5, v=0.0)
146
+ out = fn(None, core)
147
+ cue = 0.5
148
+ self.assertEqual(out["extraversion"],
149
+ interpretation.big5_trait_effect(0.93, cue))
150
+ self.assertEqual(out["neuroticism"],
151
+ interpretation.big5_trait_effect(0.52, cue))
152
+
153
+
154
+ class WiringStaticTest(unittest.TestCase):
155
+ def test_main_uses_params_for_theta_cooldown_and_ui_override(self):
156
+ self.assertIn("_autonomy_params()", SOURCE)
157
+ self.assertIn("params.get(\"cooldown_s\"", SOURCE)
158
+ self.assertIn("params.get(\"theta\"", SOURCE)
159
+ self.assertIn("\"params\"", SOURCE)
160
+
161
+
162
+ if __name__ == "__main__":
163
+ unittest.main()
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import autonomy_reason as ar
4
+
5
+ class ReasonTest(unittest.TestCase):
6
+ def test_social_gap(self):
7
+ r = ar.extract({'social_gap': 0.8})
8
+ self.assertEqual(r['code'], 'social_gap'); self.assertIn('很久', r['text'])
9
+ def test_lonely(self):
10
+ self.assertEqual(ar.extract({'lonely': 0.5})['code'], 'lonely')
11
+ def test_anxiety(self):
12
+ self.assertEqual(ar.extract({'anxiety_fear': 0.6})['code'], 'anxiety')
13
+ def test_anger(self):
14
+ self.assertEqual(ar.extract({'anger_conflict': 0.7})['code'], 'anger')
15
+ def test_repair(self):
16
+ self.assertEqual(ar.extract({'repair_urge': 0.4})['code'], 'repair')
17
+ def test_memory_due_with_title(self):
18
+ r = ar.extract({'memory_due': 0.5}, memory_title='明天体检')
19
+ self.assertEqual(r['code'], 'memory_due'); self.assertIn('明天体检', r['text'])
20
+ def test_prompt_contains_motive(self):
21
+ prompt = ar.build_prompt({'code': 'lonely', 'text': '有点闷', 'weight': 0.3})
22
+ self.assertIn('你现在主动找对方说话', prompt)
23
+ self.assertIn('因为你:孤独', prompt)
24
+ def test_fallback_contains_reason(self):
25
+ text = ar.fallback_text({'text': '很久没联系了'}, '在。')
26
+ self.assertIn('很久没联系了', text); self.assertIn('在。', text)
27
+ def test_empty_parts_default(self):
28
+ self.assertEqual(ar.extract({})['code'], 'lonely')
29
+ def test_priority_social_gap_over_memory(self):
30
+ r = ar.extract({'social_gap': 0.2, 'memory_due': 0.9}, memory_due=0.0)
31
+ self.assertEqual(r['code'], 'memory_due')
32
+
33
+ if __name__ == '__main__':
34
+ unittest.main()
@@ -0,0 +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()
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ import time, unittest
3
+ from biocore_model import autonomy_model as am
4
+
5
+ NOW = time.mktime((2026, 1, 1, 12, 0, 0, 0, 1, -1))
6
+
7
+ class SendGateTest(unittest.TestCase):
8
+ def test_fire_when_v_reaches_theta(self):
9
+ out = am.step(0.55, 0.1, sigma=0.0, theta=0.55)
10
+ self.assertTrue(out['fire'])
11
+ def test_cooldown_blocks(self):
12
+ ok, why = am.send_gate({'last_sent_ts': NOW - 10}, now=NOW)
13
+ self.assertFalse(ok); self.assertEqual(why, 'cooldown')
14
+ def test_daily_cap_blocks(self):
15
+ day = time.strftime('%Y-%m-%d', time.localtime(NOW))
16
+ ok, why = am.send_gate({'sent_day': day, 'sent_count': 3}, now=NOW)
17
+ self.assertFalse(ok); self.assertEqual(why, 'daily_cap')
18
+ def test_quiet_hours_blocks(self):
19
+ quiet = time.mktime((2026, 1, 1, 3, 0, 0, 0, 1, -1))
20
+ ok, why = am.send_gate({}, now=quiet)
21
+ self.assertFalse(ok); self.assertEqual(why, 'quiet_hours')
22
+ def test_send_disabled_blocks(self):
23
+ ok, why = am.send_gate({}, send_enabled=False)
24
+ self.assertFalse(ok); self.assertEqual(why, 'send_disabled')
25
+ def test_not_private_blocks(self):
26
+ ok, why = am.send_gate({}, private=False)
27
+ self.assertFalse(ok); self.assertEqual(why, 'not_private')
28
+ def test_allowed(self):
29
+ ok, why = am.send_gate({}, now=NOW)
30
+ self.assertTrue(ok); self.assertEqual(why, 'fired')
31
+ def test_apply_fire_resets(self):
32
+ acc = am.apply_fire({'V': 0.6, 'ticks_since_fire': 5}, now=NOW, text_len=12)
33
+ self.assertEqual(acc['V'], 0.0); self.assertEqual(acc['ticks_since_fire'], 0)
34
+ def test_no_fire_below_theta(self):
35
+ self.assertFalse(am.step(0.4, 0.0, sigma=0.0, theta=0.55)['fire'])
36
+
37
+ if __name__ == '__main__':
38
+ unittest.main()
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import autonomy_model as am
4
+
5
+ NOW = 1_700_000_000
6
+
7
+ class SilenceTest(unittest.TestCase):
8
+ def test_expect_phase(self):
9
+ self.assertEqual(am.silence_phase(1.0)['drive_mult'], 1.00)
10
+ def test_protest_phase(self):
11
+ self.assertEqual(am.silence_phase(3.0)['phase'], 'protest')
12
+ def test_loss_phase(self):
13
+ self.assertEqual(am.silence_phase(10.0)['phase'], 'loss')
14
+ def test_withdraw_phase(self):
15
+ self.assertEqual(am.silence_phase(25.0)['phase'], 'withdraw')
16
+ def test_boundary_2h(self):
17
+ self.assertEqual(am.silence_phase(2.0)['phase'], 'protest')
18
+ def test_boundary_6h(self):
19
+ self.assertEqual(am.silence_phase(6.0)['phase'], 'loss')
20
+ def test_boundary_24h(self):
21
+ self.assertEqual(am.silence_phase(24.0)['phase'], 'withdraw')
22
+ def test_silence_zero_after_reply(self):
23
+ self.assertEqual(am.silence_hours(NOW - 3600, NOW, NOW), 0.0)
24
+ def test_silence_hours(self):
25
+ self.assertAlmostEqual(am.silence_hours(NOW - 7200, NOW - 9000, NOW), 2.0)
26
+ def test_nudges_direction(self):
27
+ n = am.silence_nudges(6, 'protest', 0.8)
28
+ self.assertLess(n['valence'], 0); self.assertEqual(n['arousal'], 0.0)
29
+ self.assertEqual(n['stress'], 0.0); self.assertGreater(n['lonely'], 0)
30
+ def test_nudges_dt_capped(self):
31
+ self.assertLessEqual(am.silence_nudges(100, 'loss', 1.0)['dt_hours'], 6.0)
32
+ def test_silence_view_fields(self):
33
+ view = am.silence_view(NOW - 3 * 3600, 0, NOW)
34
+ self.assertIn('hours', view); self.assertIn('phase', view); self.assertIn('drive_mult', view)
35
+
36
+ if __name__ == '__main__':
37
+ unittest.main()
@@ -0,0 +1,35 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import autonomy_model as am
4
+
5
+ ACC = {'V': 0.2, 'theta': 0.55, 'drift': 0.1, 'leak': 0.15, 'sigma': 0.06, 'ticks_since_fire': 3}
6
+
7
+ class StatusSnapshotTest(unittest.TestCase):
8
+ def test_fields(self):
9
+ snap = am.status_snapshot(ACC, {'social_gap': 0.5})
10
+ for key in ('V', 'theta', 'drift', 'leak', 'sigma', 'ticks_since_fire', 'to_threshold', 'parts', 'gate_allowed', 'block_reason', 'last_tick_ts', 'eta_hours'):
11
+ self.assertIn(key, snap)
12
+ def test_does_not_mutate(self):
13
+ acc = dict(ACC)
14
+ am.status_snapshot(acc, {'social_gap': 0.1})
15
+ self.assertEqual(acc, ACC)
16
+ def test_to_threshold(self):
17
+ self.assertAlmostEqual(am.status_snapshot(ACC)['to_threshold'], 0.35)
18
+ def test_eta_hours(self):
19
+ self.assertAlmostEqual(am.status_snapshot(ACC)['eta_hours'], round(0.35 / 0.1 * 300 / 3600.0, 2))
20
+ def test_zero_drift_eta_none(self):
21
+ self.assertIsNone(am.status_snapshot(dict(ACC, drift=0.0))['eta_hours'])
22
+ def test_parts_include_all_weights(self):
23
+ snap = am.status_snapshot(ACC, {})
24
+ for key in am.DRIFT_WEIGHTS:
25
+ self.assertIn(key, snap['parts'])
26
+ def test_affect_field(self):
27
+ snap = am.status_snapshot({'affect': {'valence': -0.1}})
28
+ self.assertIn('affect', snap); self.assertEqual(snap['affect']['valence'], -0.1)
29
+
30
+ def test_missing_acc_no_crash(self):
31
+ snap = am.status_snapshot(None)
32
+ self.assertEqual(snap['V'], 0.0)
33
+
34
+ if __name__ == '__main__':
35
+ unittest.main()
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import autonomy_model as am
4
+
5
+ class TestTriggerTest(unittest.TestCase):
6
+ def test_sets_v(self):
7
+ out = am.test_trigger({'theta': 0.55}, reset=False)
8
+ self.assertAlmostEqual(out['V_set'], 0.495); self.assertFalse(out['reset'])
9
+ def test_reset_zero(self):
10
+ out = am.test_trigger({'theta': 0.55, 'V': 0.9}, reset=True)
11
+ self.assertEqual(out['V_set'], 0.0); self.assertTrue(out['reset'])
12
+ def test_theta_read(self):
13
+ self.assertEqual(am.test_trigger({'theta': 0.8})['theta'], 0.8)
14
+ def test_default_theta(self):
15
+ self.assertAlmostEqual(am.test_trigger({})['theta'], 0.55)
16
+ def test_missing_acc_no_crash(self):
17
+ self.assertTrue(am.test_trigger(None)['ok'])
18
+ def test_next_tick_can_fire(self):
19
+ out = am.test_trigger({'theta': 0.55})
20
+ tick = am.step(out['V_set'], 0.0, sigma=0.0, theta=out['theta'])
21
+ self.assertGreaterEqual(tick['V_raw'], 0.4) # leak applies; drift decides firing
22
+
23
+ if __name__ == '__main__':
24
+ unittest.main()