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,446 @@
1
+ # -*- coding: utf-8 -*-
2
+ """P0-1 entity disambiguation: alias merge, similarity merge, panel merge API.
3
+
4
+ Covers the pure resolver (biocore_model/entity_resolve.py), its storage wiring
5
+ (entity_graph upsert/merge), the model-facing alias pass-through (state_tag +
6
+ graph_extract) and the dashboard merge endpoint (main.py).
7
+ """
8
+ import ast
9
+ import json
10
+ import sys
11
+ import tempfile
12
+ import types
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+ from biocore_model import entity_graph as eg
17
+ from biocore_model import entity_resolve as er
18
+ from biocore_model import graph_extract as ge
19
+ from biocore_model import state_tag as st
20
+
21
+ NOW = 1800000000.0
22
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
23
+ SOURCE = MAIN.read_text(encoding="utf-8")
24
+
25
+
26
+ def _store(tmp, name="graph", threshold=None):
27
+ return eg.open_store(Path(tmp) / name / eg.SQLITE_FILE,
28
+ similarity_threshold=threshold)
29
+
30
+
31
+ class SimilarityTest(unittest.TestCase):
32
+ def test_normalize_and_ratio(self):
33
+ self.assertEqual(er.normalize(" Xiao Xia! "), "xiaoxia")
34
+ self.assertEqual(er.similarity("Xiao Xia", "xiaoxia"), 1.0)
35
+ self.assertGreaterEqual(er.similarity("老板张总", "老板张"), 0.85)
36
+ self.assertLessEqual(er.similarity("小夏", "小美"), 0.5)
37
+ self.assertEqual(er.similarity("", "小夏"), 0.0)
38
+
39
+ def test_match_priority_name_alias_similarity(self):
40
+ rows = [{"id": "e_1", "name": "小夏", "aliases": ["夏夏"]},
41
+ {"id": "e_2", "name": "老板张总", "aliases": []}]
42
+ self.assertEqual(er.match(rows, "小夏")["via"], "name")
43
+ self.assertEqual(er.match(rows, "夏夏")["via"], "alias")
44
+ self.assertEqual(er.match(rows, "老板张")["via"], "similarity")
45
+ self.assertIsNone(er.match(rows, "奶茶"))
46
+
47
+ def test_bounded_aliases(self):
48
+ out = er.bounded_aliases(["夏夏", "小夏", "夏夏", "字" * 40, ""], "小夏")
49
+ self.assertEqual(out[0], "夏夏")
50
+ self.assertEqual(len(out), 2)
51
+ self.assertEqual(len(out[1]), 20)
52
+
53
+ def test_clean_merge_ids(self):
54
+ self.assertEqual(er.clean_merge_ids({"keep_id": "a", "drop_id": "b"}),
55
+ ("a", "b"))
56
+ self.assertEqual(er.clean_merge_ids({"keep": "a", "drop": "b"}),
57
+ ("a", "b"))
58
+ self.assertEqual(er.clean_merge_ids({"keep_id": "a", "drop_id": "a"}),
59
+ ("a", ""))
60
+ self.assertEqual(er.clean_merge_ids("nope"), ("", ""))
61
+
62
+
63
+ class UpsertMergeTest(unittest.TestCase):
64
+ def test_alias_merge_sqlite(self):
65
+ with tempfile.TemporaryDirectory() as tmp:
66
+ store = _store(tmp)
67
+ try:
68
+ _, e1 = eg.upsert_entity(store, "小夏", type="person",
69
+ aliases=["夏夏"], now=100.0)
70
+ _, e2 = eg.upsert_entity(store, "夏夏", type="person",
71
+ now=200.0)
72
+ self.assertEqual(e1, e2)
73
+ self.assertEqual(eg.stats(store)["entities"], 1)
74
+ row = store._row_by_id(e1)
75
+ self.assertEqual(row["aliases"].count("夏夏"), 1)
76
+ self.assertEqual(row["last_seen"], 200.0)
77
+ finally:
78
+ store.close()
79
+
80
+ def test_similarity_merge_sqlite(self):
81
+ with tempfile.TemporaryDirectory() as tmp:
82
+ store = _store(tmp)
83
+ try:
84
+ _, e1 = eg.upsert_entity(store, "老板张总", type="person",
85
+ now=100.0)
86
+ _, e2 = eg.upsert_entity(store, "老板张", now=200.0)
87
+ self.assertEqual(e1, e2)
88
+ self.assertEqual(eg.stats(store)["entities"], 1)
89
+ self.assertIn("老板张", store._row_by_id(e1)["aliases"])
90
+ finally:
91
+ store.close()
92
+
93
+ def test_similarity_can_be_disabled(self):
94
+ with tempfile.TemporaryDirectory() as tmp:
95
+ store = _store(tmp, threshold=0)
96
+ try:
97
+ _, e1 = eg.upsert_entity(store, "老板张总")
98
+ _, e2 = eg.upsert_entity(store, "老板张")
99
+ self.assertNotEqual(e1, e2)
100
+ self.assertEqual(eg.stats(store)["entities"], 2)
101
+ finally:
102
+ store.close()
103
+
104
+ def test_candidate_alias_matches_existing(self):
105
+ with tempfile.TemporaryDirectory() as tmp:
106
+ store = _store(tmp)
107
+ try:
108
+ _, e1 = eg.upsert_entity(store, "小夏", type="person")
109
+ _, e2 = eg.upsert_entity(store, "夏夏", aliases=["小夏"])
110
+ self.assertEqual(e1, e2)
111
+ self.assertEqual(eg.stats(store)["entities"], 1)
112
+ finally:
113
+ store.close()
114
+
115
+ def test_different_people_not_merged(self):
116
+ with tempfile.TemporaryDirectory() as tmp:
117
+ store = _store(tmp)
118
+ try:
119
+ for name in ("小夏", "夏夏", "小美", "老板", "老板娘",
120
+ "张三", "李四"):
121
+ eg.upsert_entity(store, name)
122
+ self.assertEqual(eg.stats(store)["entities"], 7)
123
+ finally:
124
+ store.close()
125
+
126
+ def test_dict_graph_similarity_merge(self):
127
+ graph, e1 = eg.upsert_entity(eg.empty_graph(), "老板张总",
128
+ type="person")
129
+ graph, e2 = eg.upsert_entity(graph, "老板张")
130
+ self.assertEqual(e1, e2)
131
+ self.assertEqual(len(graph["entities"]), 1)
132
+ self.assertIn("老板张", graph["entities"][0]["aliases"])
133
+ self.assertIsNotNone(eg.find_similar(graph, "老板张"))
134
+
135
+
136
+ class SelfLoopGuardTest(unittest.TestCase):
137
+ def test_edge_between_two_spellings_is_not_stored(self):
138
+ with tempfile.TemporaryDirectory() as tmp:
139
+ store = _store(tmp)
140
+ try:
141
+ eg.upsert_entity(store, "体检", type="event",
142
+ aliases=["体检医院"])
143
+ _, edge_id = eg.upsert_edge(store, "体检", "located_at",
144
+ "体检医院", ts=100.0)
145
+ self.assertEqual(edge_id, "")
146
+ self.assertEqual(eg.stats(store)["edges"], 0)
147
+ self.assertEqual(eg.stats(store)["entities"], 1)
148
+ finally:
149
+ store.close()
150
+
151
+ def test_dict_edge_between_two_spellings_is_not_stored(self):
152
+ graph, _ = eg.upsert_entity(eg.empty_graph(), "体检", type="event",
153
+ aliases=["体检医院"])
154
+ graph, edge_id = eg.upsert_edge(graph, "体检", "located_at",
155
+ "体检医院", ts=100.0)
156
+ self.assertEqual(edge_id, "")
157
+ self.assertEqual(len(graph["edges"]), 0)
158
+
159
+
160
+ class MergeEntitiesTest(unittest.TestCase):
161
+ def test_merge_unions_fields(self):
162
+ with tempfile.TemporaryDirectory() as tmp:
163
+ store = _store(tmp)
164
+ try:
165
+ _, keep = eg.upsert_entity(store, "小夏", type="topic",
166
+ now=100.0, entry_id="m1")
167
+ _, drop = eg.upsert_entity(store, "夏夏", type="person",
168
+ now=300.0, entry_id="m2")
169
+ report = store.merge_entities(keep, drop)
170
+ self.assertTrue(report["ok"], report)
171
+ row = store._row_by_id(keep)
172
+ self.assertEqual(row["type"], "person") # topic -> specific
173
+ self.assertIn("夏夏", row["aliases"])
174
+ self.assertEqual(row["sources"], ["m1", "m2"])
175
+ self.assertEqual(row["first_seen"], 100.0)
176
+ self.assertEqual(row["last_seen"], 300.0)
177
+ self.assertIsNone(store._row_by_id(drop))
178
+ finally:
179
+ store.close()
180
+
181
+ def test_merge_rewires_edges_and_drops_self_loop(self):
182
+ with tempfile.TemporaryDirectory() as tmp:
183
+ store = _store(tmp)
184
+ try:
185
+ eg.upsert_edge(store, "小夏", "planned", "体检", ts=100.0)
186
+ eg.upsert_edge(store, "夏夏", "likes", "奶茶", ts=200.0)
187
+ eg.upsert_edge(store, "小夏", "knows", "夏夏", ts=300.0)
188
+ eg.upsert_edge(store, "夏夏", "planned", "体检", ts=100.0)
189
+ keep = store._find_entity("小夏")["id"]
190
+ drop = store._find_entity("夏夏")["id"]
191
+ report = store.merge_entities(keep, drop)
192
+ self.assertTrue(report["ok"], report)
193
+ self.assertEqual(report["edges_rewired"], 2)
194
+ self.assertEqual(report["edges_dropped"], 2)
195
+ names = {(row[0], row[1], row[2]) for row in
196
+ store._conn.execute(
197
+ "SELECT src, rel, dst FROM edges").fetchall()}
198
+ self.assertIn((keep, "planned", store._find_entity("体检")["id"]),
199
+ names)
200
+ self.assertIn((keep, "likes", store._find_entity("奶茶")["id"]),
201
+ names)
202
+ self.assertEqual(len(names), 2)
203
+ self.assertEqual(eg.stats(store)["entities"], 3)
204
+ finally:
205
+ store.close()
206
+
207
+ def test_merge_errors(self):
208
+ with tempfile.TemporaryDirectory() as tmp:
209
+ store = _store(tmp)
210
+ try:
211
+ _, eid = eg.upsert_entity(store, "小夏")
212
+ self.assertFalse(store.merge_entities(eid, eid)["ok"])
213
+ self.assertFalse(store.merge_entities(eid, "e_missing")["ok"])
214
+ self.assertFalse(store.merge_entities("", "")["ok"])
215
+ self.assertIsNotNone(store._row_by_id(eid))
216
+ finally:
217
+ store.close()
218
+
219
+ def test_dict_graph_merge(self):
220
+ graph, keep = eg.upsert_entity(eg.empty_graph(), "小夏",
221
+ type="person")
222
+ graph, drop = eg.upsert_entity(graph, "夏夏")
223
+ graph, _ = eg.upsert_edge(graph, "夏夏", "likes", "奶茶", ts=5.0)
224
+ graph, report = eg.merge_entities(graph, keep, drop)
225
+ self.assertTrue(report["ok"], report)
226
+ self.assertEqual(len(graph["entities"]), 2)
227
+ self.assertEqual(len(graph["edges"]), 1)
228
+ self.assertEqual(graph["edges"][0]["src"], keep)
229
+ self.assertIn("夏夏", graph["entities"][0]["aliases"])
230
+
231
+
232
+ def _load_page_graph_merge():
233
+ tree = ast.parse(SOURCE)
234
+ cls = next(node for node in tree.body
235
+ if isinstance(node, ast.ClassDef) and node.name == "BioCorePlugin")
236
+ node = next(item for item in cls.body
237
+ if isinstance(item, ast.AsyncFunctionDef)
238
+ and item.name == "page_graph_merge")
239
+ clone = ast.AsyncFunctionDef(name=node.name, args=node.args, body=node.body,
240
+ decorator_list=[], returns=node.returns,
241
+ type_comment=None, lineno=node.lineno,
242
+ col_offset=node.col_offset,
243
+ end_lineno=node.end_lineno,
244
+ end_col_offset=node.end_col_offset)
245
+ ns = {"logger": types.SimpleNamespace(
246
+ info=lambda *a, **k: None, warning=lambda *a, **k: None)}
247
+ exec(compile(ast.Module(body=[clone], type_ignores=[]),
248
+ "<page_graph_merge>", "exec"), ns)
249
+ return ns[node.name], ns
250
+
251
+
252
+ PAGE_MERGE, MERGE_NS = _load_page_graph_merge()
253
+
254
+
255
+ class _FakeRequest:
256
+ def __init__(self, payload):
257
+ self.payload = payload
258
+
259
+ async def get_json(self, silent=False):
260
+ return self.payload
261
+
262
+
263
+ class _FakePlugin:
264
+ def __init__(self, path):
265
+ self._path = path
266
+
267
+ def _graph_db_path(self):
268
+ return self._path
269
+
270
+
271
+ class PanelMergeApiTest(unittest.IsolatedAsyncioTestCase):
272
+ async def _call(self, plugin, payload):
273
+ module = types.ModuleType("quart")
274
+ module.request = _FakeRequest(payload)
275
+ previous = sys.modules.get("quart")
276
+ sys.modules["quart"] = module
277
+ try:
278
+ return await PAGE_MERGE(plugin)
279
+ finally:
280
+ if previous is None:
281
+ sys.modules.pop("quart", None)
282
+ else:
283
+ sys.modules["quart"] = previous
284
+
285
+ async def test_merge_endpoint_merges_two_entities(self):
286
+ with tempfile.TemporaryDirectory() as tmp:
287
+ path = Path(tmp) / "graph" / eg.SQLITE_FILE
288
+ store = eg.open_store(path)
289
+ try:
290
+ _, keep = eg.upsert_entity(store, "小夏", type="person")
291
+ _, drop = eg.upsert_entity(store, "夏夏")
292
+ eg.upsert_edge(store, "夏夏", "likes", "奶茶")
293
+ finally:
294
+ store.close()
295
+ out = await self._call(_FakePlugin(path),
296
+ {"keep_id": keep, "drop_id": drop})
297
+ self.assertEqual(out["status"], "ok", out)
298
+ self.assertTrue(out["data"]["ok"])
299
+ self.assertEqual(out["data"]["aliases"], 1)
300
+ store = eg.open_store(path)
301
+ try:
302
+ self.assertIsNone(store._row_by_id(drop))
303
+ self.assertIn("夏夏", store._row_by_id(keep)["aliases"])
304
+ self.assertEqual(eg.stats(store)["entities"], 2)
305
+ self.assertEqual(eg.stats(store)["edges"], 1)
306
+ finally:
307
+ store.close()
308
+
309
+ async def test_merge_endpoint_rejects_bad_payload(self):
310
+ with tempfile.TemporaryDirectory() as tmp:
311
+ path = Path(tmp) / "graph" / eg.SQLITE_FILE
312
+ out = await self._call(_FakePlugin(path), {})
313
+ self.assertEqual(out["status"], "error")
314
+ out = await self._call(_FakePlugin(path),
315
+ {"keep_id": "e_nope", "drop_id": "e_also"})
316
+ self.assertEqual(out["status"], "error")
317
+ self.assertEqual(out["message"], "entity not found")
318
+
319
+ def test_route_registered(self):
320
+ self.assertIn("/astrbot_plugin_biocore/page/graph/merge", SOURCE)
321
+ self.assertIn("self.page_graph_merge", SOURCE)
322
+
323
+
324
+ class AliasPassThroughTest(unittest.TestCase):
325
+ def test_state_tag_keeps_bounded_aliases(self):
326
+ raw = json.dumps({"dims": {"goal": 0.1}, "c": 0.8, "entities": [
327
+ {"name": "小夏", "type": "人物",
328
+ "aliases": ["夏夏", "小夏", "夏夏", "字" * 40]}]},
329
+ ensure_ascii=False)
330
+ out = st.parse("好。\n" + raw)
331
+ entity = out["entities"][0]
332
+ self.assertEqual(entity["name"], "小夏")
333
+ self.assertEqual(entity["aliases"][0], "夏夏")
334
+ self.assertEqual(len(entity["aliases"]), 2)
335
+ self.assertEqual(len(entity["aliases"][1]), 20)
336
+
337
+ def test_graph_extract_and_apply_carry_aliases(self):
338
+ tag = {"dims": {"goal": 0.2}, "c": 0.9, "entities": [
339
+ {"name": "小夏", "type": "人物", "aliases": ["夏夏"]},
340
+ {"name": "体检", "type": "事件"}],
341
+ "relations": [{"src": "小夏", "rel": "have_checkup",
342
+ "dst": "体检", "when": "周五"}]}
343
+ extracted = ge.extract(tag, now=NOW)
344
+ self.assertEqual(extracted["entities"][0]["aliases"], ["夏夏"])
345
+ with tempfile.TemporaryDirectory() as tmp:
346
+ store = _store(tmp)
347
+ try:
348
+ ge.apply(store, extracted, now=NOW, source_entry="m1")
349
+ row = store._find_entity("小夏")
350
+ self.assertIn("夏夏", row["aliases"])
351
+ _, eid = eg.upsert_entity(store, "夏夏")
352
+ self.assertEqual(eid, row["id"])
353
+ self.assertEqual(eg.stats(store)["entities"], 2)
354
+ finally:
355
+ store.close()
356
+
357
+
358
+ TURNS = [
359
+ {"entities": [{"name": "小夏", "type": "人物"}],
360
+ "relations": [{"src": "小夏", "rel": "planned", "dst": "海边",
361
+ "when": "周末"}]},
362
+ {"entities": [{"name": "夏夏", "type": "人物", "aliases": ["小夏"]}],
363
+ "relations": [{"src": "夏夏", "rel": "have_checkup", "dst": "体检",
364
+ "when": "周五"}]},
365
+ {"entities": [{"name": "小夏", "type": "人物"},
366
+ {"name": "海边", "type": "地点"}]},
367
+ {"entities": [{"name": "海边", "type": "地点"}]},
368
+ {"entities": [{"name": "体检", "type": "事件",
369
+ "aliases": ["体检医院"]}]},
370
+ {"entities": [{"name": "老板张总", "type": "人物"}],
371
+ "relations": [{"src": "老板张总", "rel": "likes", "dst": "奶茶",
372
+ "when": "今天"}]},
373
+ {"entities": [{"name": "夏夏", "type": "人物"},
374
+ {"name": "体检医院", "type": "地点"}],
375
+ "relations": [{"src": "夏夏", "rel": "planned", "dst": "体检医院",
376
+ "when": "昨天"}]},
377
+ {"entities": [{"name": "小夏", "type": "人物"},
378
+ {"name": "奶茶", "type": "话题"}]},
379
+ {"entities": [{"name": "老板张", "type": "人物"}]},
380
+ {"entities": [{"name": "夏夏姐", "type": "人物", "aliases": ["夏夏"]}],
381
+ "relations": [{"src": "夏夏姐", "rel": "likes", "dst": "奶茶",
382
+ "when": "今天"}]},
383
+ {"entities": [{"name": "海边", "type": "地点"},
384
+ {"name": "奶茶", "type": "话题"}]},
385
+ {"entities": [{"name": "小夏", "type": "人物"},
386
+ {"name": "体检", "type": "事件"}],
387
+ "relations": [{"src": "小夏", "rel": "have_checkup", "dst": "体检",
388
+ "when": "周五"}]},
389
+ {"entities": [{"name": "老板张总", "type": "人物"}]},
390
+ {"entities": [{"name": "夏夏", "type": "人物"}]},
391
+ {"entities": [{"name": "体检医院", "type": "地点"}]},
392
+ {"entities": [{"name": "老板张", "type": "人物"},
393
+ {"name": "奶茶", "type": "话题"}],
394
+ "relations": [{"src": "老板张", "rel": "likes", "dst": "奶茶",
395
+ "when": "今天"}]},
396
+ {"entities": [{"name": "小夏", "type": "人物"},
397
+ {"name": "海边", "type": "地点"}]},
398
+ {"entities": [{"name": "夏夏姐", "type": "人物"}]},
399
+ {"entities": [{"name": "体检", "type": "事件"},
400
+ {"name": "体检医院", "type": "地点"}],
401
+ "relations": [{"src": "体检", "rel": "located_at", "dst": "体检医院",
402
+ "when": "昨天"}]},
403
+ {"entities": [{"name": "奶茶", "type": "话题"}]},
404
+ ]
405
+
406
+
407
+ class TwentyTurnSimulationTest(unittest.TestCase):
408
+ def test_twenty_turns_do_not_inflate_entities(self):
409
+ mentioned = []
410
+ with tempfile.TemporaryDirectory() as tmp:
411
+ store = _store(tmp)
412
+ try:
413
+ for index, turn in enumerate(TURNS):
414
+ payload = {"dims": {"goal": 0.1}, "c": 0.8,
415
+ "entities": turn["entities"]}
416
+ if turn.get("relations"):
417
+ payload["relations"] = turn["relations"]
418
+ tag = st.parse("好。\n" + json.dumps(payload,
419
+ ensure_ascii=False))
420
+ extracted = ge.extract(tag, now=NOW + index * 60.0)
421
+ ge.apply(store, extracted, now=NOW + index * 60.0,
422
+ source_entry="m%d" % index)
423
+ mentioned.extend(row["name"]
424
+ for row in turn["entities"])
425
+ entities = eg.stats(store)["entities"]
426
+ self.assertGreater(len(mentioned), 20)
427
+ self.assertLessEqual(entities, 5, eg.stats(store))
428
+ self.assertGreaterEqual(eg.stats(store)["edges"], 4)
429
+ known = {row[0] for row in store._conn.execute(
430
+ "SELECT id FROM entities").fetchall()}
431
+ for row in store._conn.execute(
432
+ "SELECT src, dst FROM edges").fetchall():
433
+ self.assertIn(row[0], known)
434
+ self.assertIn(row[1], known)
435
+ row = store._find_entity("小夏")
436
+ self.assertIn("夏夏", row["aliases"])
437
+ self.assertIn("夏夏姐", row["aliases"])
438
+ self.assertEqual(eg.stats(store)["entities"], 5)
439
+ self.assertEqual(store._find_entity("老板张")["id"],
440
+ store._find_entity("老板张总")["id"])
441
+ finally:
442
+ store.close()
443
+
444
+
445
+ if __name__ == "__main__":
446
+ unittest.main()
@@ -0,0 +1,173 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+
4
+ from biocore_model import envy as ev
5
+
6
+
7
+ def signals(stage="close", fairness=0.5, third=None, self_eval=0.5):
8
+ return {
9
+ "relation_stage": stage,
10
+ "fairness": fairness,
11
+ "third_party": third,
12
+ "self_evaluation": self_eval,
13
+ }
14
+
15
+
16
+ class EnvyLevelTest(unittest.TestCase):
17
+ def test_mild_attention_level(self):
18
+ out = ev.assess(signals("friend", 0.5,
19
+ {"presence": 0.9, "closeness": 0.5}, 0.5))
20
+ self.assertEqual(out["kind"], "轻微在意")
21
+ self.assertEqual(out["kind_key"], "mild")
22
+
23
+ def test_comparison_level(self):
24
+ out = ev.assess(signals("close", 0.2,
25
+ {"presence": 0.9, "closeness": 0.6}, 0.4))
26
+ self.assertEqual(out["kind"], "比较")
27
+ self.assertGreaterEqual(out["intensity"], ev.COMPARE_THRESHOLD)
28
+ self.assertLess(out["intensity"], ev.ENVY_THRESHOLD)
29
+
30
+ def test_envy_level(self):
31
+ out = ev.assess(signals("intimate", 0.1,
32
+ {"presence": 1.0, "closeness": 0.8}, 0.3))
33
+ self.assertEqual(out["kind"], "嫉妒")
34
+ self.assertEqual(out["kind_key"], "envy")
35
+
36
+ def test_below_trigger_has_no_level(self):
37
+ out = ev.assess(signals("stranger", 0.9, False, 0.9))
38
+ self.assertEqual(out["kind"], "")
39
+ self.assertEqual(out["behavior"], "")
40
+ self.assertLess(out["intensity"], ev.TRIGGER_THRESHOLD)
41
+
42
+ def test_thresholds_are_ordered(self):
43
+ self.assertLess(ev.TRIGGER_THRESHOLD, ev.COMPARE_THRESHOLD)
44
+ self.assertLess(ev.COMPARE_THRESHOLD, ev.ENVY_THRESHOLD)
45
+
46
+ def test_kind_of_maps_three_levels(self):
47
+ self.assertEqual(ev.kind_of(0.10), "")
48
+ self.assertEqual(ev.kind_of(0.30), "轻微在意")
49
+ self.assertEqual(ev.kind_of(0.55), "比较")
50
+ self.assertEqual(ev.kind_of(0.90), "嫉妒")
51
+
52
+
53
+ class EnvyBehaviorTest(unittest.TestCase):
54
+ def test_close_comparison_asks(self):
55
+ out = ev.assess(signals("close", 0.2,
56
+ {"presence": 0.9, "closeness": 0.6}, 0.4))
57
+ self.assertEqual(out["behavior"], "探问")
58
+
59
+ def test_distant_mild_attention_withdraws(self):
60
+ out = ev.assess(signals("friend", 0.5,
61
+ {"presence": 0.9, "closeness": 0.5}, 0.5))
62
+ self.assertEqual(out["behavior"], "收敛")
63
+
64
+ def test_high_envy_nitpicks(self):
65
+ out = ev.assess(signals("intimate", 0.1,
66
+ {"presence": 1.0, "closeness": 0.8}, 0.3))
67
+ self.assertEqual(out["behavior"], "挑刺")
68
+
69
+ def test_low_self_evaluation_suppresses_envy(self):
70
+ out = ev.assess(signals("intimate", 0.1,
71
+ {"presence": 1.0, "closeness": 1.0}, 0.1))
72
+ self.assertEqual(out["kind"], "嫉妒")
73
+ self.assertEqual(out["behavior"], "压下")
74
+
75
+ def test_behavior_always_known(self):
76
+ for stage in ("stranger", "friend", "close", "intimate"):
77
+ for self_eval in (0.0, 0.5, 1.0):
78
+ out = ev.assess(signals(stage, 0.3,
79
+ {"presence": 1.0,
80
+ "closeness": 0.8}, self_eval))
81
+ self.assertIn(out["behavior"], ev.BEHAVIORS + ("",))
82
+
83
+
84
+ class EnvyModulationTest(unittest.TestCase):
85
+ def test_relationship_stage_modulates_intensity(self):
86
+ base = {"fairness": 0.4,
87
+ "third_party": {"presence": 0.7, "closeness": 0.5},
88
+ "self_evaluation": 0.5}
89
+ distant = ev.assess(dict(base, relation_stage="stranger"))
90
+ close = ev.assess(dict(base, relation_stage="intimate"))
91
+ self.assertGreater(close["intensity"], distant["intensity"])
92
+
93
+ def test_unfairness_raises_intensity(self):
94
+ fair = ev.assess(signals("close", 0.9,
95
+ {"presence": 0.8, "closeness": 0.5}, 0.5))
96
+ unfair = ev.assess(signals("close", 0.1,
97
+ {"presence": 0.8, "closeness": 0.5}, 0.5))
98
+ self.assertGreater(unfair["intensity"], fair["intensity"])
99
+
100
+ def test_third_person_is_required(self):
101
+ silent = ev.assess(signals("intimate", 0.1, False, 0.2))
102
+ mentioned = ev.assess(signals("intimate", 0.1,
103
+ {"presence": 1.0, "closeness": 0.8}, 0.2))
104
+ self.assertEqual(silent["intensity"], 0.0)
105
+ self.assertGreater(mentioned["intensity"], 0.0)
106
+
107
+ def test_stronger_self_evaluation_buffers(self):
108
+ fragile = ev.assess(signals("intimate", 0.1,
109
+ {"presence": 1.0, "closeness": 0.8}, 0.1))
110
+ steady = ev.assess(signals("intimate", 0.1,
111
+ {"presence": 1.0, "closeness": 0.8}, 0.9))
112
+ self.assertGreater(fragile["intensity"], steady["intensity"])
113
+
114
+ def test_intensity_is_bounded(self):
115
+ for stage in ("stranger", "intimate"):
116
+ for self_eval in (-5, 0, 0.5, 1, 5):
117
+ out = ev.assess(signals(stage, -5,
118
+ {"presence": 5, "closeness": 5},
119
+ self_eval))
120
+ self.assertGreaterEqual(out["intensity"], 0.0)
121
+ self.assertLessEqual(out["intensity"], 1.0)
122
+
123
+ def test_deterministic(self):
124
+ first = ev.assess(signals("close", 0.2,
125
+ {"presence": 0.9, "closeness": 0.6}, 0.4))
126
+ second = ev.assess(signals("close", 0.2,
127
+ {"presence": 0.9, "closeness": 0.6}, 0.4))
128
+ self.assertEqual(first, second)
129
+
130
+
131
+ class EnvySwitchTest(unittest.TestCase):
132
+ def test_disabled_bool(self):
133
+ out = ev.assess(signals("intimate", 0.1,
134
+ {"presence": 1.0, "closeness": 0.8}, 0.2),
135
+ config=False)
136
+ self.assertFalse(out["enabled"])
137
+ self.assertEqual(out["kind"], "")
138
+ self.assertEqual(out["intensity"], 0.0)
139
+ self.assertEqual(out["behavior"], "")
140
+ self.assertTrue(out["reason"])
141
+
142
+ def test_disabled_config_dict(self):
143
+ out = ev.assess(signals("intimate", 0.1,
144
+ {"presence": 1.0, "closeness": 0.8}, 0.2),
145
+ config={"enabled": False})
146
+ self.assertFalse(out["enabled"])
147
+ self.assertEqual(out["kind"], "")
148
+
149
+ def test_threshold_can_be_raised(self):
150
+ out = ev.assess(signals("friend", 0.5,
151
+ {"presence": 0.9, "closeness": 0.5}, 0.5),
152
+ config={"threshold": 0.8})
153
+ self.assertEqual(out["kind"], "")
154
+
155
+ def test_malformed_inputs_do_not_crash(self):
156
+ for bad in (None, "bad", [], {"relation_stage": None,
157
+ "third_party": "??",
158
+ "fairness": "x"}):
159
+ out = ev.assess(bad)
160
+ self.assertIn(out["kind"], ev.KINDS + ("",))
161
+ self.assertIn(out["behavior"], ev.BEHAVIORS + ("",))
162
+
163
+ def test_view_fields(self):
164
+ out = ev.assess(signals("close", 0.2,
165
+ {"presence": 0.9, "closeness": 0.6}, 0.4))
166
+ panel = ev.view(out)
167
+ for key in ("enabled", "kind", "kind_key", "intensity", "behavior",
168
+ "reason"):
169
+ self.assertIn(key, panel)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ unittest.main()