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,231 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-205 follow-up: proactive_tick policy lives in the python pipeline.
3
+
4
+ Covers disabled switches, gap/hourly-cap/cooldown, trust threshold, pending
5
+ commitments, text composition and cooldown booking. No Node required.
6
+ """
7
+ import json
8
+ import tempfile
9
+ import time
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ from biocore_runtime import pipeline as pl
14
+
15
+ NOW_MS = 1_800_000_000_000
16
+ HOUR_MS = 3600 * 1000
17
+ LAST_HUMAN_MS = NOW_MS - 4 * HOUR_MS
18
+
19
+
20
+ def switches(**overrides):
21
+ base = {
22
+ "proactiveEnabled": True,
23
+ "proactiveSend": True,
24
+ "proactiveMinGapMinutes": 180,
25
+ "proactivePerHour": 2,
26
+ "proactiveMinTrust": 0.6,
27
+ "proactiveMaxChars": 60,
28
+ "proactiveRequireOpenLoop": True,
29
+ "proactiveIntervalMinutes": 30,
30
+ }
31
+ base.update(overrides)
32
+ return base
33
+
34
+
35
+ def make_store(tmp, session_id="s1", trust=0.8, texts=None, now_ms=NOW_MS):
36
+ root = Path(tmp) / "memory"
37
+ sessions = root / "sessions" / pl.sanitize_session_id(session_id)
38
+ sessions.mkdir(parents=True, exist_ok=True)
39
+ rows = []
40
+ texts = texts if texts is not None else ("明天要去体检,记得问我结果",)
41
+ for index, text in enumerate(texts):
42
+ rows.append({"id": "e%d" % index, "text": text, "role": "user",
43
+ "ts": now_ms / 1000.0 - index * 60, "deleted": False})
44
+ (sessions / "memory.json").write_text(
45
+ json.dumps({"entries": rows}), encoding="utf-8")
46
+ states = root.parent / "sessions"
47
+ states.mkdir(parents=True, exist_ok=True)
48
+ (states / (pl.sanitize_session_id(session_id) + ".json")).write_text(
49
+ json.dumps({"relationship": {"trust": trust, "closeness": trust}}),
50
+ encoding="utf-8")
51
+ return root
52
+
53
+
54
+ class ShapeTest(unittest.TestCase):
55
+ def test_output_contract(self):
56
+ with tempfile.TemporaryDirectory() as tmp:
57
+ store = make_store(tmp)
58
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(), str(store), now=NOW_MS)
59
+ for key in ("ok", "should_send", "text", "reason", "gap_minutes"):
60
+ self.assertIn(key, out)
61
+ self.assertTrue(out["ok"])
62
+ self.assertTrue(out["should_send"])
63
+ self.assertEqual(out["reason"], "ok")
64
+ self.assertAlmostEqual(out["gap_minutes"], 240.0, places=1)
65
+ self.assertIn("体检", out["text"])
66
+
67
+ def test_dispatch_route(self):
68
+ with tempfile.TemporaryDirectory() as tmp:
69
+ store = make_store(tmp)
70
+ out = pl.dispatch("proactive_tick", {
71
+ "session_id": "s1", "last_human_ts": LAST_HUMAN_MS,
72
+ "switches": switches(), "store": str(store), "now": NOW_MS})
73
+ self.assertTrue(out["should_send"])
74
+
75
+
76
+ class SwitchTest(unittest.TestCase):
77
+ def test_disabled(self):
78
+ with tempfile.TemporaryDirectory() as tmp:
79
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS,
80
+ switches(proactiveEnabled=False),
81
+ str(make_store(tmp)), now=NOW_MS)
82
+ self.assertFalse(out["should_send"])
83
+ self.assertEqual(out["reason"], "disabled")
84
+
85
+ def test_send_off(self):
86
+ with tempfile.TemporaryDirectory() as tmp:
87
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS,
88
+ switches(proactiveSend=False),
89
+ str(make_store(tmp)), now=NOW_MS)
90
+ self.assertFalse(out["should_send"])
91
+ self.assertEqual(out["reason"], "send_off")
92
+
93
+ def test_no_session(self):
94
+ with tempfile.TemporaryDirectory() as tmp:
95
+ out = pl.proactive_tick("", LAST_HUMAN_MS, switches(),
96
+ str(make_store(tmp)), now=NOW_MS)
97
+ self.assertFalse(out["should_send"])
98
+ self.assertEqual(out["reason"], "no_session")
99
+
100
+
101
+ class TimingTest(unittest.TestCase):
102
+ def test_too_soon(self):
103
+ with tempfile.TemporaryDirectory() as tmp:
104
+ out = pl.proactive_tick("s1", NOW_MS - 10 * 60 * 1000, switches(),
105
+ str(make_store(tmp)), now=NOW_MS)
106
+ self.assertEqual(out["reason"], "too_soon")
107
+ self.assertAlmostEqual(out["gap_minutes"], 10.0, places=1)
108
+
109
+ def test_hourly_cap(self):
110
+ with tempfile.TemporaryDirectory() as tmp:
111
+ store = make_store(tmp)
112
+ (store / "proactive.json").write_text(json.dumps({
113
+ "lastSentAt": NOW_MS - 200 * 60 * 1000,
114
+ "sent": [NOW_MS - 30 * 60 * 1000, NOW_MS - 40 * 60 * 1000],
115
+ }), encoding="utf-8")
116
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(), str(store),
117
+ now=NOW_MS)
118
+ self.assertEqual(out["reason"], "hourly_cap")
119
+
120
+ def test_cooldown(self):
121
+ with tempfile.TemporaryDirectory() as tmp:
122
+ store = make_store(tmp)
123
+ (store / "proactive.json").write_text(json.dumps({
124
+ "lastSentAt": NOW_MS - 30 * 60 * 1000, "sent": [],
125
+ }), encoding="utf-8")
126
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(), str(store),
127
+ now=NOW_MS)
128
+ self.assertEqual(out["reason"], "cooldown")
129
+
130
+ def test_seconds_timestamp_accepted(self):
131
+ with tempfile.TemporaryDirectory() as tmp:
132
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS / 1000.0, switches(),
133
+ str(make_store(tmp)), now=NOW_MS)
134
+ self.assertTrue(out["should_send"])
135
+ self.assertAlmostEqual(out["gap_minutes"], 240.0, places=1)
136
+
137
+
138
+ class RelationshipTest(unittest.TestCase):
139
+ def test_not_close(self):
140
+ with tempfile.TemporaryDirectory() as tmp:
141
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(),
142
+ str(make_store(tmp, trust=0.2)),
143
+ now=NOW_MS)
144
+ self.assertEqual(out["reason"], "not_close")
145
+ self.assertAlmostEqual(out["trust"], 0.2, places=3)
146
+
147
+ def test_explicit_state_file(self):
148
+ with tempfile.TemporaryDirectory() as tmp:
149
+ store = make_store(tmp, trust=0.1)
150
+ alt = Path(tmp) / "alt.json"
151
+ alt.write_text(json.dumps({"relationship": {"trust": 0.9}}),
152
+ encoding="utf-8")
153
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(), str(store),
154
+ now=NOW_MS, state_file=str(alt))
155
+ self.assertTrue(out["should_send"])
156
+
157
+ def test_missing_state_uses_default_trust(self):
158
+ with tempfile.TemporaryDirectory() as tmp:
159
+ store = Path(tmp) / "memory"
160
+ (store / "sessions" / "s1").mkdir(parents=True, exist_ok=True)
161
+ (store / "sessions" / "s1" / "memory.json").write_text(
162
+ json.dumps({"entries": [{"text": "明天要体检",
163
+ "ts": time.time()}]}), encoding="utf-8")
164
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(), str(store),
165
+ now=NOW_MS)
166
+ self.assertEqual(out["reason"], "not_close")
167
+
168
+
169
+ class CommitmentTest(unittest.TestCase):
170
+ def test_no_commitment(self):
171
+ with tempfile.TemporaryDirectory() as tmp:
172
+ out = pl.proactive_tick(
173
+ "s1", LAST_HUMAN_MS, switches(),
174
+ str(make_store(tmp, texts=("今天天气不错", "晚饭吃了面"))),
175
+ now=NOW_MS)
176
+ self.assertEqual(out["reason"], "no_commitment")
177
+
178
+ def test_optional_commitment_falls_back(self):
179
+ with tempfile.TemporaryDirectory() as tmp:
180
+ out = pl.proactive_tick(
181
+ "s1", LAST_HUMAN_MS,
182
+ switches(proactiveRequireOpenLoop=False),
183
+ str(make_store(tmp, texts=("今天天气不错",))), now=NOW_MS)
184
+ self.assertTrue(out["should_send"])
185
+ self.assertIn("在忙吗", out["text"])
186
+
187
+ def test_due_hint_preferred(self):
188
+ with tempfile.TemporaryDirectory() as tmp:
189
+ texts = ("记得给朋友回消息", "明天要去体检")
190
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(),
191
+ str(make_store(tmp, texts=texts)),
192
+ now=NOW_MS)
193
+ self.assertTrue(out["should_send"])
194
+ self.assertIn("明天要去体检", out["text"])
195
+
196
+ def test_max_chars_cap(self):
197
+ with tempfile.TemporaryDirectory() as tmp:
198
+ long_text = "明天要做的事情" * 10
199
+ out = pl.proactive_tick("s1", LAST_HUMAN_MS,
200
+ switches(proactiveMaxChars=24),
201
+ str(make_store(tmp, texts=(long_text,))),
202
+ now=NOW_MS)
203
+ self.assertTrue(out["should_send"])
204
+ self.assertLessEqual(len(out["text"]), 24)
205
+
206
+
207
+ class BookingTest(unittest.TestCase):
208
+ def test_booking_written_and_second_tick_cooldowns(self):
209
+ with tempfile.TemporaryDirectory() as tmp:
210
+ store = make_store(tmp)
211
+ first = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(),
212
+ str(store), now=NOW_MS)
213
+ self.assertTrue(first["should_send"])
214
+ state = json.loads((store / "proactive.json").read_text(encoding="utf-8"))
215
+ self.assertEqual(state["lastSentAt"], float(NOW_MS))
216
+ self.assertTrue(state["lastText"])
217
+ second = pl.proactive_tick("s1", LAST_HUMAN_MS, switches(),
218
+ str(store), now=NOW_MS + 60 * 1000)
219
+ self.assertFalse(second["should_send"])
220
+ self.assertEqual(second["reason"], "cooldown")
221
+
222
+ def test_no_booking_when_skipped(self):
223
+ with tempfile.TemporaryDirectory() as tmp:
224
+ store = make_store(tmp)
225
+ pl.proactive_tick("s1", NOW_MS - 60 * 1000, switches(), str(store),
226
+ now=NOW_MS)
227
+ self.assertFalse((store / "proactive.json").exists())
228
+
229
+
230
+ if __name__ == "__main__":
231
+ unittest.main()
@@ -0,0 +1,79 @@
1
+ # -*- coding: utf-8 -*-
2
+ """HUMAN-1 scheduler tests: dimensions, lazy decay, ranking."""
3
+ import unittest
4
+
5
+ from biocore_model import scheduler as sc
6
+
7
+
8
+ def items():
9
+ return [
10
+ {"id": "a", "due_ts": 1000.0, "entity": "u1", "channel": "chat", "weight": 1.0, "last_ts": 0.0},
11
+ {"id": "b", "due_ts": 1000.0, "entity": "u2", "channel": "chat", "weight": 1.0, "last_ts": 0.0},
12
+ {"id": "c", "due_ts": 100.0, "entity": "u1", "channel": "note", "weight": 1.0, "last_ts": 0.0},
13
+ ]
14
+
15
+
16
+ class TimeTest(unittest.TestCase):
17
+ def test_time_score_peaks_at_due(self):
18
+ row = {"due_ts": 1000.0}
19
+ self.assertAlmostEqual(sc.time_score(row, 1000.0), 1.0)
20
+ self.assertLess(sc.time_score(row, 1000.0 + 3600.0), 1.0)
21
+
22
+ def test_time_score_symmetric_distance(self):
23
+ row = {"due_ts": 1000.0}
24
+ self.assertAlmostEqual(sc.time_score(row, 1000.0 - 7200.0),
25
+ sc.time_score(row, 1000.0 + 7200.0))
26
+
27
+
28
+ class DimensionTest(unittest.TestCase):
29
+ def test_entity_match_beats_miss(self):
30
+ self.assertGreater(sc.entity_score({"entity": "u1"}, "u1"),
31
+ sc.entity_score({"entity": "u1"}, "u2"))
32
+
33
+ def test_channel_match_beats_miss(self):
34
+ self.assertGreater(sc.channel_score({"channel": "chat"}, "chat"),
35
+ sc.channel_score({"channel": "chat"}, "note"))
36
+
37
+ def test_no_filter_is_neutral(self):
38
+ self.assertEqual(sc.entity_score({"entity": "u1"}), 1.0)
39
+ self.assertEqual(sc.channel_score({"channel": "chat"}), 1.0)
40
+
41
+ def test_custom_weights_change_order(self):
42
+ now = 1000.0
43
+ default = sc.score(items()[1], now, entity="u1", channel="chat")
44
+ entity_heavy = sc.score(items()[1], now, entity="u1", channel="chat",
45
+ weights={"time": 0.1, "entity": 0.8, "channel": 0.1})
46
+ self.assertNotAlmostEqual(default, entity_heavy)
47
+
48
+
49
+ class DecayRankTest(unittest.TestCase):
50
+ def test_lazy_decay_halves_in_one_half_life(self):
51
+ row = sc.lazy_decay({"weight": 1.0, "last_ts": 0.0}, 12 * 3600.0)
52
+ self.assertAlmostEqual(row["weight"], 0.5, places=4)
53
+
54
+ def test_lazy_decay_does_not_mutate(self):
55
+ source = {"weight": 1.0, "last_ts": 0.0, "id": "x"}
56
+ sc.lazy_decay(source, 999999.0)
57
+ self.assertEqual(source, {"weight": 1.0, "last_ts": 0.0, "id": "x"})
58
+
59
+ def test_rank_is_descending_and_deterministic(self):
60
+ rows = sc.rank(items(), 1000.0, entity="u1", channel="chat")
61
+ scores = [row["score"] for row in rows]
62
+ self.assertEqual(scores, sorted(scores, reverse=True))
63
+ self.assertEqual(rows, sc.rank(items(), 1000.0, entity="u1", channel="chat"))
64
+
65
+ def test_pick_and_limit(self):
66
+ self.assertEqual(sc.pick(items(), 1000.0, entity="u1", channel="chat")["item"]["id"], "a")
67
+ self.assertEqual(len(sc.rank(items(), 1000.0, limit=2)), 2)
68
+ self.assertEqual(sc.rank([], 1000.0), [])
69
+
70
+ def test_due_window_filters(self):
71
+ rows = sc.due(items(), 1000.0, window_seconds=600.0)
72
+ ids = [row["id"] for row in rows]
73
+ self.assertIn("a", ids)
74
+ self.assertIn("b", ids)
75
+ self.assertNotIn("c", ids)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ unittest.main()
@@ -0,0 +1,105 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+
4
+ from biocore_model import secrecy as sc
5
+
6
+ NOW = 1_700_000_000
7
+
8
+
9
+ class SecrecyDecisionTest(unittest.TestCase):
10
+ def test_low_sensitivity_deep_relation_safe_setting_tells(self):
11
+ out = sc.decide(0.10, 0.85, 0.90)
12
+ self.assertEqual(out["action"], "tell")
13
+
14
+ def test_high_sensitivity_shallow_relation_unsafe_setting_withholds(self):
15
+ out = sc.decide(0.90, 0.15, 0.15)
16
+ self.assertEqual(out["action"], "withhold")
17
+
18
+ def test_high_sensitivity_close_relation_reframes(self):
19
+ out = sc.decide(0.80, 0.85, 0.50)
20
+ self.assertEqual(out["action"], "reframe")
21
+
22
+ def test_action_always_known(self):
23
+ for sentence in (None, -5, 5, "x"):
24
+ out = sc.decide(sentence, sentence, sentence)
25
+ self.assertIn(out["action"], sc.ACTIONS)
26
+
27
+ def test_decision_is_deterministic(self):
28
+ first = sc.decide(0.8, 0.85, 0.5)
29
+ second = sc.decide(0.8, 0.85, 0.5)
30
+ self.assertEqual(first, second)
31
+
32
+ def test_safer_setting_raises_tell_score(self):
33
+ unsafe = sc.tell_score(0.5, 0.5, 0.2)
34
+ safe = sc.tell_score(0.5, 0.5, 0.9)
35
+ self.assertGreater(safe, unsafe)
36
+
37
+ def test_confidence_is_bounded(self):
38
+ out = sc.decide(0.9, 0.1, 0.1)
39
+ self.assertGreaterEqual(out["confidence"], 0.0)
40
+ self.assertLessEqual(out["confidence"], 1.0)
41
+
42
+
43
+ class SecrecyCostTest(unittest.TestCase):
44
+ def test_telling_has_no_hidden_cost(self):
45
+ self.assertEqual(sc.hidden_cost("tell", 0.9, 0.9), 0.0)
46
+ self.assertEqual(sc.decide(0.1, 0.9, 0.9)["pressure_add"], 0.0)
47
+
48
+ def test_withholding_has_a_cost(self):
49
+ self.assertGreater(sc.hidden_cost("withhold", 0.8, 0.5), 0.0)
50
+
51
+ def test_reframe_costs_less_than_withholding(self):
52
+ withheld = sc.hidden_cost("withhold", 0.8, 0.5)
53
+ softened = sc.hidden_cost("reframe", 0.8, 0.5)
54
+ self.assertGreater(withheld, softened)
55
+ self.assertGreater(softened, 0.0)
56
+
57
+
58
+ class SecrecyPressureTest(unittest.TestCase):
59
+ def test_pressure_accumulates_across_hidden_turns(self):
60
+ first = sc.update(sc.initial(NOW - 3600), 0.9, 0.15, 0.15, now=NOW)
61
+ second = sc.update(first, 0.9, 0.15, 0.15, now=NOW + 3600)
62
+ self.assertGreater(first["pressure"], 0.0)
63
+ self.assertGreater(second["pressure"], first["pressure"])
64
+ self.assertEqual(second["withholds"], 2)
65
+
66
+ def test_same_turn_is_idempotent(self):
67
+ once = sc.update(sc.initial(NOW - 10), 0.9, 0.15, 0.15, now=NOW)
68
+ twice = sc.update(once, 0.9, 0.15, 0.15, now=NOW)
69
+ self.assertEqual(twice["pressure"], once["pressure"])
70
+ self.assertEqual(twice["withholds"], once["withholds"])
71
+
72
+ def test_telling_does_not_raise_withhold_count(self):
73
+ state = sc.update(sc.initial(NOW - 10), 0.1, 0.9, 0.9, now=NOW)
74
+ self.assertEqual(state["withholds"], 0)
75
+ self.assertEqual(state["pressure"], 0.0)
76
+
77
+ def test_pressure_is_capped_at_one(self):
78
+ state = dict(sc.initial(NOW - 10), pressure=0.99)
79
+ loaded = sc.update(state, 0.9, 0.15, 0.15, now=NOW)
80
+ self.assertLessEqual(loaded["pressure"], 1.0)
81
+
82
+ def test_pressure_decays_over_time(self):
83
+ state = dict(sc.initial(NOW), pressure=0.50)
84
+ rested = sc.update(state, 0.1, 0.9, 0.9, now=NOW + 10 * 3600)
85
+ self.assertAlmostEqual(rested["pressure"], 0.30)
86
+
87
+ def test_contained_flag(self):
88
+ self.assertTrue(sc.contained(dict(sc.initial(NOW), pressure=0.30)))
89
+ self.assertFalse(sc.contained(dict(sc.initial(NOW), pressure=0.70)))
90
+
91
+ def test_view_fields(self):
92
+ state = sc.update(sc.initial(NOW - 10), 0.9, 0.15, 0.15, now=NOW)
93
+ panel = sc.view(state)
94
+ for key in ("pressure", "contained", "withholds", "last_action",
95
+ "last_pressure_add"):
96
+ self.assertIn(key, panel)
97
+
98
+ def test_bad_inputs_do_not_crash(self):
99
+ state = sc.update(None, None, None, None)
100
+ self.assertIn(state["last_action"], sc.ACTIONS)
101
+ self.assertEqual(sc.hidden_cost(None, None, None), 0.0)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ unittest.main()
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import self_esteem as se
4
+
5
+ NOW = 1_700_000_000
6
+
7
+ class SelfEsteemTest(unittest.TestCase):
8
+ def test_initial_baseline(self):
9
+ state = se.initial(NOW)
10
+ self.assertAlmostEqual(state['value'], se.BASELINE)
11
+ def test_praised_raises(self):
12
+ self.assertAlmostEqual(se.update(se.initial(NOW), 'praised', NOW)['value'], 0.61)
13
+ def test_replied_raises(self):
14
+ self.assertAlmostEqual(se.update(se.initial(NOW), 'replied', NOW)['value'], 0.61)
15
+ def test_ignored_lowers(self):
16
+ self.assertAlmostEqual(se.update(se.initial(NOW), 'ignored', NOW)['value'], 0.5)
17
+ def test_insulted_lowers(self):
18
+ self.assertAlmostEqual(se.update(se.initial(NOW), 'insulted', NOW)['value'], 0.5)
19
+ def test_decay_toward_baseline(self):
20
+ state = {'value': 0.2, 'baseline': se.BASELINE, 'last_decay_ts': NOW - 1}
21
+ self.assertGreater(se.decay(state, NOW)['value'], 0.2)
22
+ def test_decay_same_now_idempotent(self):
23
+ state = {'value': 0.3, 'baseline': se.BASELINE, 'last_decay_ts': NOW}
24
+ self.assertEqual(se.decay(state, NOW)['value'], 0.3)
25
+ def test_update_same_now_does_not_double(self):
26
+ state = se.update(se.initial(NOW), 'praised', NOW)
27
+ again = se.update(se.initial(NOW), 'praised', NOW)
28
+ self.assertAlmostEqual(again['value'], state['value'])
29
+ def test_clamp_upper(self):
30
+ state = {'value': 0.99, 'baseline': se.BASELINE}
31
+ self.assertLessEqual(se.update(state, 'praised', NOW)['value'], 1.0)
32
+ def test_clamp_lower(self):
33
+ state = {'value': 0.01, 'baseline': se.BASELINE}
34
+ self.assertGreaterEqual(se.update(state, 'insulted', NOW)['value'], 0.0)
35
+ def test_unknown_event_no_change(self):
36
+ self.assertAlmostEqual(se.update(se.initial(NOW), 'bogus', NOW)['value'], se.BASELINE)
37
+ def test_confidence_low_esteem(self):
38
+ self.assertLess(se.confidence_delta(0.2), 0) # lower esteem -> lower confidence
39
+ def test_view_fields(self):
40
+ view = se.view(se.update(se.initial(NOW), 'praised', NOW))
41
+ for key in ('value', 'baseline', 'last_event', 'confidence_delta'):
42
+ self.assertIn(key, view)
43
+
44
+ if __name__ == '__main__':
45
+ unittest.main()