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,116 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Global bio-state hydration: restart without cores still evolves intero/needs."""
3
+ import ast
4
+ import json
5
+ import tempfile
6
+ import types
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from biocore_model import bio_tick as bt
11
+ from biocore_model import intero_core as ic
12
+ from biocore_model import ui_settings
13
+
14
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
15
+ SOURCE = MAIN.read_text(encoding="utf-8")
16
+
17
+
18
+ def _load_helpers(state_dir):
19
+ tree = ast.parse(SOURCE)
20
+ cls = next(n for n in tree.body
21
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
22
+ wanted = ("_bio_state_key", "_make_global_bio_core", "_ensure_bio_core",
23
+ "_persist_global_bio_state", "_bio_cores_for_tick")
24
+ ns = {"types": types, "STATE_DIR": Path(state_dir)}
25
+ fns = {}
26
+ for node in cls.body:
27
+ if isinstance(node, ast.FunctionDef) and node.name in wanted:
28
+ clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
29
+ decorator_list=[], returns=node.returns,
30
+ type_comment=None, lineno=node.lineno,
31
+ col_offset=node.col_offset,
32
+ end_lineno=node.end_lineno,
33
+ end_col_offset=node.end_col_offset)
34
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<biohyd>", "exec"), ns)
35
+ fns[node.name] = ns[node.name]
36
+ return fns
37
+
38
+
39
+ class FakePlugin:
40
+ def __init__(self, data_dir, state_dir):
41
+ self.data_dir = str(data_dir)
42
+ self.cores = {}
43
+ self._global_bio_core = None
44
+ self.core_calls = []
45
+ self._fns = _load_helpers(state_dir)
46
+ for name, fn in self._fns.items():
47
+ setattr(self, name, types.MethodType(fn, self))
48
+
49
+ def _core(self, event):
50
+ key = str(getattr(event, "unified_msg_origin", ""))
51
+ self.core_calls.append(key)
52
+ dummy = types.SimpleNamespace(umo=key)
53
+ return dummy
54
+
55
+
56
+ class HydrationTest(unittest.TestCase):
57
+ def test_no_cores_creates_global_state_and_ticks(self):
58
+ with tempfile.TemporaryDirectory() as data, tempfile.TemporaryDirectory() as states:
59
+ plugin = FakePlugin(data, states)
60
+ cores = plugin._bio_cores_for_tick()
61
+ self.assertEqual(len(cores), 1)
62
+ core = cores[0]
63
+ now = 100000.0
64
+ for index in range(5):
65
+ bt.bio_tick(core, now + index * 10.0)
66
+ plugin._persist_global_bio_state()
67
+ saved = ui_settings.load(plugin).get("bio_state") or {}
68
+ for dim in ic.DIMS:
69
+ self.assertIn(dim, saved.get("intero") or {})
70
+ self.assertGreater(saved.get("last_bio_tick_ts", 0.0), 0.0)
71
+ self.assertIn("needs", saved)
72
+ discomfort = ic.discomfort(saved["intero"])["total"]
73
+ self.assertIsInstance(discomfort, float)
74
+
75
+ def test_global_core_reused_between_ticks(self):
76
+ with tempfile.TemporaryDirectory() as data, tempfile.TemporaryDirectory() as states:
77
+ plugin = FakePlugin(data, states)
78
+ first = plugin._ensure_bio_core()
79
+ second = plugin._ensure_bio_core()
80
+ self.assertIs(first, second)
81
+
82
+ def test_hydrates_last_umo_first(self):
83
+ with tempfile.TemporaryDirectory() as data, tempfile.TemporaryDirectory() as states:
84
+ plugin = FakePlugin(data, states)
85
+ ui_settings.save(plugin, {"autonomy": {"accumulator": {
86
+ "last_umo": "platform:private:u1"}}})
87
+ core = plugin._ensure_bio_core()
88
+ self.assertEqual(plugin.core_calls[-1], "platform:private:u1")
89
+ self.assertIn("platform:private:u1", plugin.cores)
90
+ self.assertIs(core, plugin.cores["platform:private:u1"])
91
+
92
+ def test_hydrates_newest_state_file_when_no_last_umo(self):
93
+ with tempfile.TemporaryDirectory() as data, tempfile.TemporaryDirectory() as states:
94
+ state = Path(states) / "platform_private_u2.json"
95
+ state.write_text(json.dumps({"v": 0.1, "a": 0.4}), encoding="utf-8")
96
+ plugin = FakePlugin(data, states)
97
+ plugin._ensure_bio_core()
98
+ self.assertEqual(plugin.core_calls[-1], "platform_private_u2")
99
+
100
+ def test_existing_cores_win(self):
101
+ with tempfile.TemporaryDirectory() as data, tempfile.TemporaryDirectory() as states:
102
+ plugin = FakePlugin(data, states)
103
+ existing = types.SimpleNamespace(umo="existing")
104
+ plugin.cores = {"existing": existing}
105
+ self.assertIs(plugin._ensure_bio_core(), existing)
106
+ self.assertEqual(plugin._bio_cores_for_tick(), [existing])
107
+
108
+ def test_page_debug_uses_global_fallback(self):
109
+ # The source must call _ensure_bio_core when cores is empty.
110
+ self.assertIn("core = self._ensure_bio_core()", SOURCE)
111
+ self.assertIn("_bio_cores_for_tick()", SOURCE)
112
+ self.assertIn("_persist_global_bio_state()", SOURCE)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ unittest.main()
@@ -0,0 +1,181 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-223: memory governance (decay/index/consolidate/concepts) + relationship."""
3
+ import json
4
+ import tempfile
5
+ import time
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_runtime import governance
10
+ from biocore_runtime import pipeline
11
+ from biocore_runtime import relationship_state
12
+
13
+
14
+ def write_json(path, data):
15
+ path = Path(path)
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
18
+
19
+
20
+ def memory(entry_id, text, ts, importance=0.5, concepts=None):
21
+ row = {"id": entry_id, "text": text, "role": "user", "ts": ts,
22
+ "importance": importance, "hits": 1}
23
+ if concepts is not None:
24
+ row["concepts"] = list(concepts)
25
+ return row
26
+
27
+
28
+ class StoreCase(unittest.TestCase):
29
+ NOW = 1700000000.0
30
+
31
+ def setUp(self):
32
+ self.tmp = tempfile.TemporaryDirectory()
33
+ self.data_dir = Path(self.tmp.name) / "data"
34
+ self.store = self.data_dir / "memory"
35
+ self.persona = Path(self.tmp.name) / "missing_persona.json"
36
+ self.write_session("s1", [
37
+ memory("fresh", "今天买了咖啡", self.NOW - 60, 0.9),
38
+ memory("old", "很久以前的事", self.NOW - 400 * 86400, 0.2),
39
+ memory("mid", "苹果很甜", self.NOW - 86400, 0.5),
40
+ ])
41
+
42
+ def tearDown(self):
43
+ self.tmp.cleanup()
44
+
45
+ def write_session(self, session_id, entries):
46
+ write_json(self.store / "sessions" / session_id / "memory.json",
47
+ {"session_id": session_id, "entries": entries})
48
+
49
+ def payload(self, session_id="s1", **extra):
50
+ data = {"store": str(self.store), "session_id": session_id,
51
+ "persona_profile": str(self.persona)}
52
+ data.update(extra)
53
+ return data
54
+
55
+ def turn_end(self, session_id="s1", user_text="", **extra):
56
+ return pipeline.dispatch("turn_end", self.payload(
57
+ session_id, user_text=user_text, assistant_text="嗯。",
58
+ capture=False, **extra))
59
+
60
+
61
+ class DecayTest(StoreCase):
62
+ def test_decay_marks_but_never_deletes(self):
63
+ before = len(governance._entries(self.store))
64
+ out = governance.run(self.store, now=self.NOW, force=True)
65
+ after = len(governance._entries(self.store))
66
+ self.assertEqual(before, after)
67
+ self.assertGreaterEqual(out["decay"]["cold"], 1)
68
+ self.assertEqual(out["decay"]["deleted"], 0)
69
+ self.assertLess(governance.weight_of(self.store, "old"), 1.0)
70
+
71
+ def test_fresh_entry_keeps_full_weight(self):
72
+ governance.run(self.store, now=self.NOW, force=True)
73
+ self.assertEqual(governance.weight_of(self.store, "fresh"), 1.0)
74
+
75
+ def test_evict_delete_flag_does_not_delete(self):
76
+ write_json(self.data_dir / "flags.json", {"evict_delete": True})
77
+ before = len(governance._entries(self.store))
78
+ governance.run(self.store, now=self.NOW, force=True)
79
+ self.assertEqual(len(governance._entries(self.store)), before)
80
+
81
+ def test_governance_off_skips(self):
82
+ write_json(self.data_dir / "flags.json", {"governance": False})
83
+ out = governance.run(self.store, now=self.NOW, force=True)
84
+ self.assertTrue(out.get("skipped"))
85
+ self.assertFalse((self.store / "governance.json").exists())
86
+
87
+
88
+ class IndexAndConceptTest(StoreCase):
89
+ def test_index_lookup_hits_entry(self):
90
+ governance.run(self.store, now=self.NOW, force=True)
91
+ self.assertIn("mid", governance.lookup(self.store, "苹果"))
92
+ doc = json.loads((self.store / "index.json").read_text(encoding="utf-8"))
93
+ self.assertTrue(doc["terms"])
94
+
95
+ def test_inject_uses_index_and_counts_hit(self):
96
+ governance.run(self.store, now=self.NOW, force=True)
97
+ out = pipeline.inject_block("s1", str(self.store), query="苹果",
98
+ mode="query", top_k=3)
99
+ self.assertEqual(out["ids"][0], "mid")
100
+ gov = json.loads((self.store / "governance.json").read_text(encoding="utf-8"))
101
+ self.assertGreaterEqual(gov.get("index_hits", 0), 1)
102
+
103
+ def test_remember_stores_concepts(self):
104
+ pipeline.tool_call("remember", {"text": "苹果和咖啡都很好"},
105
+ {"root": str(self.store)}, session_id="s2")
106
+ doc = json.loads((self.store / "sessions" / "s2" / "memory.json")
107
+ .read_text(encoding="utf-8"))
108
+ self.assertTrue(doc["entries"][0].get("concepts"))
109
+
110
+ def test_concept_filter_narrows_candidates(self):
111
+ entries = [{"id": "a", "concepts": ["苹果", "甜"]},
112
+ {"id": "b", "concepts": ["香蕉", "黄"]}]
113
+ kept = pipeline._concept_filter(entries, "苹果")
114
+ self.assertEqual([row["id"] for row in kept], ["a"])
115
+
116
+
117
+ class ConsolidateTest(StoreCase):
118
+ def test_consolidate_is_idempotent(self):
119
+ self.write_session("s2", [
120
+ memory("c1", "注意安全", self.NOW - 3),
121
+ memory("c2", "出门注意安全", self.NOW - 2),
122
+ memory("c3", "记得注意安全", self.NOW - 1),
123
+ ])
124
+ first = governance.run(self.store, now=self.NOW, force=True)
125
+ second = governance.run(self.store, now=self.NOW + 1, force=True)
126
+ self.assertEqual(first["consolidate"]["candidates"],
127
+ second["consolidate"]["candidates"])
128
+ self.assertTrue(second["consolidate"]["candidates"] >= 1)
129
+ self.assertFalse(second["consolidate"]["changed"])
130
+ doc = json.loads((self.store / "consolidate.json").read_text(encoding="utf-8"))
131
+ self.assertGreaterEqual(doc["runs"], 2)
132
+
133
+ def test_graph_and_aggregate(self):
134
+ self.write_session("s2", [memory("g1", "苹果 苹果 甜", self.NOW - 5),
135
+ memory("g2", "苹果 很甜", self.NOW - 4)])
136
+ out = governance.run(self.store, now=self.NOW, force=True)
137
+ self.assertGreaterEqual(out["graph"]["nodes"], 1)
138
+ self.assertGreaterEqual(out["aggregate"]["sessions"], 2)
139
+
140
+
141
+ class RelationshipTest(StoreCase):
142
+ def test_warm_text_raises_closeness(self):
143
+ self.turn_end(user_text="谢谢你陪我,今天很开心")
144
+ rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
145
+ self.assertGreater(rel["relationship"]["closeness"], 0.5)
146
+ self.assertGreater(rel["relationship"]["trust"], 0.5)
147
+
148
+ def test_conflict_text_raises_conflict(self):
149
+ self.turn_end(user_text="我很生气,别说了")
150
+ rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
151
+ self.assertGreater(rel["relationship"]["conflict"], 0.0)
152
+ self.assertLess(rel["relationship"]["trust"], 0.5)
153
+
154
+ def test_repair_lowers_conflict(self):
155
+ self.turn_end(user_text="我很生气")
156
+ self.turn_end(user_text="对不起,是我不好")
157
+ rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
158
+ summary = relationship_state.summary(self.store, rel["relationship"])
159
+ self.assertGreaterEqual(summary["repairs"], 1)
160
+ self.assertLess(rel["relationship"]["conflict"], 0.02)
161
+
162
+ def test_relationship_off_keeps_values(self):
163
+ write_json(self.data_dir / "flags.json", {"relationship_governance": False})
164
+ self.turn_end(user_text="谢谢你陪我")
165
+ rel = json.loads((self.store / "relationship.json").read_text(encoding="utf-8"))
166
+ self.assertAlmostEqual(rel["relationship"]["closeness"], 0.5, places=4)
167
+ self.assertAlmostEqual(rel["relationship"]["trust"], 0.5, places=4)
168
+ self.assertFalse((self.store / "relationship_memory.json").exists())
169
+
170
+ def test_state_exposes_numbers(self):
171
+ self.turn_end(user_text="谢谢你陪我")
172
+ out = pipeline.dispatch("state", self.payload())
173
+ body = out["state"]
174
+ self.assertIn("governance", body)
175
+ self.assertIn("decay", body["governance"])
176
+ self.assertIn("relationship_summary", body)
177
+ self.assertGreaterEqual(body["relationship_summary"]["events"], 1)
178
+
179
+
180
+ if __name__ == "__main__":
181
+ unittest.main()
@@ -0,0 +1,133 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+
4
+ from biocore_model import granularity as g
5
+
6
+
7
+ class GranularityMappingTest(unittest.TestCase):
8
+ def test_aggrieved_mapping(self):
9
+ out = g.refine({"sadness": 0.50, "anger": 0.20, "joy": 0.05})
10
+ self.assertEqual(out["primary"], "sadness")
11
+ self.assertEqual(out["labels"], ["委屈"])
12
+ self.assertFalse(out["fallback"])
13
+
14
+ def test_disappointed_mapping(self):
15
+ out = g.refine({"sadness": 0.50, "trust": 0.20, "anger": 0.05})
16
+ self.assertEqual(out["labels"], ["失望"])
17
+
18
+ def test_ignored_mapping(self):
19
+ out = g.refine({"loneliness": 0.60, "sadness": 0.15})
20
+ self.assertEqual(out["primary"], "loneliness")
21
+ self.assertEqual(out["labels"], ["被忽视"])
22
+
23
+ def test_offended_mapping(self):
24
+ out = g.refine({"anger": 0.55, "trust": 0.10},
25
+ context={"offense": 0.50})
26
+ self.assertEqual(out["labels"], ["被冒犯"])
27
+
28
+ def test_uneasy_mapping(self):
29
+ out = g.refine({"fear": 0.55, "loneliness": 0.15})
30
+ self.assertEqual(out["labels"], ["不安"])
31
+
32
+ def test_worn_out_mapping(self):
33
+ out = g.refine({"sadness": 0.50}, context={"fatigue": 0.60})
34
+ self.assertEqual(out["labels"], ["疲惫"])
35
+
36
+ def test_offended_needs_appraisal_cue(self):
37
+ # anger alone is a prototype, not an appraisal of being offended
38
+ out = g.refine({"anger": 0.90})
39
+ self.assertEqual(out["labels"], [])
40
+ self.assertTrue(out["fallback"])
41
+ self.assertEqual(out["display"], "生气")
42
+
43
+ def test_weak_anger_does_not_invent_aggrieved(self):
44
+ out = g.refine({"sadness": 0.90, "anger": 0.05})
45
+ self.assertEqual(out["labels"], [])
46
+ self.assertEqual(out["display"], "难过")
47
+
48
+
49
+ class GranularityFallbackAndSwitchTest(unittest.TestCase):
50
+ def test_fallback_to_six_prototypes(self):
51
+ out = g.refine({"joy": 0.90, "trust": 0.10})
52
+ self.assertEqual(out["primary"], "joy")
53
+ self.assertTrue(out["fallback"])
54
+ self.assertEqual(out["display"], "开心")
55
+ self.assertEqual(out["secondary"], [])
56
+
57
+ def test_disabled_bool_keeps_prototype(self):
58
+ out = g.refine({"sadness": 0.50, "anger": 0.20}, config=False)
59
+ self.assertFalse(out["enabled"])
60
+ self.assertEqual(out["labels"], [])
61
+ self.assertEqual(out["display"], "难过")
62
+
63
+ def test_disabled_config_dict(self):
64
+ out = g.refine({"loneliness": 0.60, "sadness": 0.15},
65
+ config={"enabled": False})
66
+ self.assertFalse(out["enabled"])
67
+ self.assertTrue(out["fallback"])
68
+ self.assertEqual(out["display"], "孤独")
69
+
70
+ def test_empty_scores(self):
71
+ out = g.refine({})
72
+ self.assertEqual(out["primary"], "")
73
+ self.assertEqual(out["display"], "")
74
+ self.assertTrue(out["fallback"])
75
+
76
+ def test_unknown_keys_ignored(self):
77
+ out = g.refine({"sadness": 0.60, "bogus": 0.99})
78
+ self.assertEqual(out["primary"], "sadness")
79
+ self.assertEqual(out["labels"], [])
80
+
81
+ def test_primary_tie_is_deterministic(self):
82
+ self.assertEqual(g.primary({"joy": 0.50, "trust": 0.50}), "joy")
83
+ self.assertEqual(g.primary({"trust": 0.50, "joy": 0.50}), "joy")
84
+
85
+ def test_custom_rules_replace_table(self):
86
+ rules = ({"key": "hollow", "label": "空落", "primary": "loneliness",
87
+ "weights": {"loneliness": 1.0}, "threshold": 0.30},)
88
+ out = g.refine({"loneliness": 0.50}, rules=rules)
89
+ self.assertEqual(out["labels"], ["空落"])
90
+ default = g.refine({"loneliness": 0.50, "sadness": 0.20})
91
+ self.assertEqual(default["labels"], ["被忽视"])
92
+
93
+ def test_max_labels_limit(self):
94
+ out = g.refine({"sadness": 0.60, "anger": 0.20},
95
+ context={"fatigue": 0.60},
96
+ config={"max_labels": 1})
97
+ self.assertEqual(out["labels"], ["委屈"])
98
+
99
+ def test_deterministic_same_input(self):
100
+ scores = {"sadness": 0.55, "anger": 0.25}
101
+ first = g.refine(scores)
102
+ second = g.refine(dict(scores))
103
+ self.assertEqual(first, second)
104
+
105
+
106
+ class GranularityViewAndStateTest(unittest.TestCase):
107
+ def test_view_line(self):
108
+ panel = g.view({"sadness": 0.50, "anger": 0.20})
109
+ self.assertEqual(panel["display"], "委屈")
110
+ self.assertEqual(panel["line"], "委屈(难过)")
111
+ self.assertEqual(panel["secondary"][0]["key"], "aggrieved")
112
+
113
+ def test_view_fallback_line(self):
114
+ panel = g.view({"joy": 0.90})
115
+ self.assertEqual(panel["line"], "开心")
116
+ self.assertTrue(panel["fallback"])
117
+
118
+ def test_from_state_reaches_six_prototypes(self):
119
+ out = g.from_state({"v": -0.70, "a": 0.30, "d": 0.20,
120
+ "social": 0.30, "stress": 0.60})
121
+ self.assertEqual(out["primary"], "sadness")
122
+ self.assertEqual(out["primary_zh"], "难过")
123
+
124
+ def test_from_state_with_fatigue_cue(self):
125
+ out = g.from_state({"v": -0.70, "a": 0.30, "d": 0.20,
126
+ "social": 0.30, "stress": 0.60},
127
+ context={"fatigue": 0.80})
128
+ self.assertEqual(out["labels"], ["疲惫"])
129
+ self.assertFalse(out["fallback"])
130
+
131
+
132
+ if __name__ == "__main__":
133
+ unittest.main()
@@ -0,0 +1,224 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-135: inline-tag entity/relation extraction tests."""
3
+ import json
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ from biocore_model import entity_graph as eg
9
+ from biocore_model import graph_extract as ge
10
+ from biocore_model import state_tag
11
+
12
+ NOW = 1704844800.0 # 2024-01-10 (Wednesday, local Asia/Shanghai)
13
+ DAY = 86400.0
14
+
15
+
16
+ def _tag(entities=None, relations=None, confidence=0.8):
17
+ return {"dims": {"goal": 0.1}, "confidence": confidence,
18
+ "entities": entities or [], "relations": relations or []}
19
+
20
+
21
+ class TypeMappingTest(unittest.TestCase):
22
+ def test_chinese_type_to_canonical_and_back(self):
23
+ self.assertEqual(ge.canonical_type("人物"), "person")
24
+ self.assertEqual(ge.canonical_type("地点"), "place")
25
+ self.assertEqual(ge.canonical_type("事件"), "event")
26
+ self.assertEqual(ge.canonical_type("话题"), "topic")
27
+ self.assertEqual(ge.canonical_type("unknown-thing"), "topic")
28
+ self.assertEqual(ge.type_label("person"), "人物")
29
+ self.assertEqual(ge.type_label("place"), "地点")
30
+
31
+ def test_relation_label(self):
32
+ self.assertEqual(ge.rel_label("planned"), "计划")
33
+ self.assertEqual(ge.rel_label("custom_rel"), "custom_rel")
34
+
35
+
36
+ class WhenTest(unittest.TestCase):
37
+ def test_relative_words(self):
38
+ self.assertEqual(ge.when_to_ts("今天", NOW), NOW)
39
+ self.assertEqual(ge.when_to_ts("昨天", NOW), NOW - DAY)
40
+ self.assertEqual(ge.when_to_ts("前天", NOW), NOW - 2 * DAY)
41
+ self.assertEqual(ge.when_to_ts("明天", NOW), NOW + DAY)
42
+ self.assertEqual(ge.when_to_ts("3天前", NOW), NOW - 3 * DAY)
43
+ self.assertEqual(ge.when_to_ts("2小时前", NOW), NOW - 7200.0)
44
+
45
+ def test_weekday_words(self):
46
+ self.assertEqual(ge.when_to_ts("周五", NOW), NOW - 5 * DAY)
47
+ self.assertEqual(ge.when_to_ts("上周五", NOW), NOW - 12 * DAY)
48
+
49
+ def test_unknown_or_empty(self):
50
+ self.assertEqual(ge.when_to_ts("", NOW), 0.0)
51
+ self.assertEqual(ge.when_to_ts("某个时候", NOW), 0.0)
52
+ self.assertEqual(ge.when_to_ts(None, NOW), 0.0)
53
+
54
+
55
+ class ExtractTest(unittest.TestCase):
56
+ def test_entities_with_types(self):
57
+ out = ge.extract(_tag(entities=[{"name": "小夏", "type": "人物"},
58
+ {"name": "体检", "type": "事件"}]), now=NOW)
59
+ self.assertEqual([row["name"] for row in out["entities"]],
60
+ ["小夏", "体检"])
61
+ self.assertEqual(out["entities"][0]["type"], "person")
62
+ self.assertEqual(out["entities"][1]["type"], "event")
63
+
64
+ def test_bare_string_entities_are_topic(self):
65
+ out = ge.extract(_tag(entities=["海边", "打雷"]), now=NOW)
66
+ self.assertEqual([row["type"] for row in out["entities"]],
67
+ ["topic", "topic"])
68
+
69
+ def test_entity_cap_and_dedupe(self):
70
+ out = ge.extract(_tag(entities=["a", "b", "c", "d", "e", "A"]), now=NOW)
71
+ self.assertEqual(len(out["entities"]), 3)
72
+ self.assertEqual([row["name"] for row in out["entities"]],
73
+ ["a", "b", "c"])
74
+
75
+ def test_relation_cap_and_validation(self):
76
+ relations = [
77
+ {"src": "a", "rel": "planned", "dst": "b", "when": "周五"},
78
+ {"src": "b", "rel": "likes", "dst": "c", "when": "昨天"},
79
+ {"src": "c", "rel": "knows", "dst": "a"},
80
+ {"src": "a", "rel": "r4", "dst": "b"},
81
+ {"src": "bad", "rel": "", "dst": "x"},
82
+ "garbage",
83
+ ]
84
+ out = ge.extract(_tag(entities=["a", "b", "c"], relations=relations),
85
+ now=NOW)
86
+ self.assertLessEqual(len(out["relations"]), ge.MAX_RELATIONS)
87
+ self.assertEqual(len(out["relations"]), 3)
88
+ self.assertEqual(out["relations"][0]["rel"], "planned")
89
+ self.assertEqual(out["relations"][0]["ts"], NOW - 5 * DAY)
90
+
91
+ def test_relation_endpoints_join_until_cap(self):
92
+ out = ge.extract(_tag(entities=["a"],
93
+ relations=[{"src": "a", "rel": "r", "dst": "b"}]),
94
+ now=NOW)
95
+ names = [row["name"] for row in out["entities"]]
96
+ self.assertEqual(names, ["a", "b"])
97
+ self.assertEqual(len(out["relations"]), 1)
98
+
99
+ def test_relation_with_unknown_endpoint_dropped_when_full(self):
100
+ out = ge.extract(_tag(entities=["a", "b", "c"],
101
+ relations=[{"src": "a", "rel": "r", "dst": "z"}]),
102
+ now=NOW)
103
+ self.assertEqual(out["relations"], [])
104
+
105
+ def test_dirty_input_is_safe(self):
106
+ for value in (None, {}, [], "x", {"entities": "x", "relations": 5}):
107
+ out = ge.extract(value, now=NOW)
108
+ self.assertEqual(out["entities"], [])
109
+ self.assertEqual(out["relations"], [])
110
+
111
+
112
+ class ApplyTest(unittest.TestCase):
113
+ def test_apply_writes_sqlite_graph(self):
114
+ with tempfile.TemporaryDirectory() as tmp:
115
+ store = eg.open_store(Path(tmp) / "graph" / eg.SQLITE_FILE)
116
+ try:
117
+ extracted = ge.extract(_tag(
118
+ entities=[{"name": "小夏", "type": "人物"},
119
+ {"name": "体检", "type": "事件"}],
120
+ relations=[{"src": "小夏", "rel": "planned",
121
+ "dst": "体检", "when": "周五"}]), now=NOW)
122
+ applied = ge.apply(store, extracted, now=NOW, source_entry="m1")
123
+ self.assertEqual(applied["entities"], 2)
124
+ self.assertEqual(applied["relations"], 1)
125
+ stats = eg.stats(store)
126
+ self.assertEqual(stats["entities"], 2)
127
+ self.assertEqual(stats["edges"], 1)
128
+ row = store._conn.execute(
129
+ "SELECT ts, confidence, source_entry FROM edges").fetchone()
130
+ self.assertEqual(row[0], NOW - 5 * DAY)
131
+ self.assertEqual(row[2], "m1")
132
+ found = eg.neighbors(store, "小夏", hops=1)
133
+ self.assertEqual({item["name"] for item in found["entities"]},
134
+ {"小夏", "体检"})
135
+ finally:
136
+ store.close()
137
+
138
+ def test_apply_is_idempotent(self):
139
+ with tempfile.TemporaryDirectory() as tmp:
140
+ store = eg.open_store(Path(tmp) / "graph" / eg.SQLITE_FILE)
141
+ try:
142
+ extracted = ge.extract(_tag(entities=["a", "b"],
143
+ relations=[{"src": "a", "rel": "r",
144
+ "dst": "b",
145
+ "when": "今天"}]),
146
+ now=NOW)
147
+ ge.apply(store, extracted, now=NOW, source_entry="m1")
148
+ ge.apply(store, extracted, now=NOW, source_entry="m1")
149
+ stats = eg.stats(store)
150
+ self.assertEqual(stats["entities"], 2)
151
+ self.assertEqual(stats["edges"], 1)
152
+ finally:
153
+ store.close()
154
+
155
+ def test_apply_empty_is_noop(self):
156
+ with tempfile.TemporaryDirectory() as tmp:
157
+ store = eg.open_store(Path(tmp) / "graph" / eg.SQLITE_FILE)
158
+ try:
159
+ self.assertEqual(ge.apply(store, {}, now=NOW),
160
+ {"entities": 0, "relations": 0})
161
+ finally:
162
+ store.close()
163
+
164
+
165
+ class StateTagIntegrationTest(unittest.TestCase):
166
+ def test_instruction_exposes_graph_fields(self):
167
+ text = state_tag.instruction()
168
+ self.assertIn('"entities"', text)
169
+ self.assertIn('"relations"', text)
170
+ self.assertIn("最多 3", text)
171
+
172
+ def test_parse_graph_fields_bounded(self):
173
+ raw = json.dumps({
174
+ "dims": {"goal": 0.1}, "c": 0.8,
175
+ "entities": [{"name": "a", "type": "人物"}, "b", "c", "d"],
176
+ "relations": [{"src": "a", "rel": "r", "dst": "b",
177
+ "when": "昨天"},
178
+ {"src": "b", "rel": "r2", "dst": "c"},
179
+ {"src": "c", "rel": "r3", "dst": "d"},
180
+ {"src": "d", "rel": "r4", "dst": "a"}]},
181
+ ensure_ascii=False)
182
+ out = state_tag.parse("reply\n" + raw)
183
+ self.assertEqual(len(out["entities"]), 3)
184
+ self.assertEqual(len(out["relations"]), 3)
185
+ self.assertEqual(out["entities"][0]["name"], "a")
186
+
187
+ def test_end_to_end_tag_to_graph(self):
188
+ raw = json.dumps({
189
+ "dims": {"goal": 0.2}, "c": 0.9,
190
+ "entities": [{"name": "小夏", "type": "人物"},
191
+ {"name": "医院", "type": "地点"}],
192
+ "relations": [{"src": "小夏", "rel": "located_at",
193
+ "dst": "医院", "when": "昨天"}]},
194
+ ensure_ascii=False)
195
+ tag = state_tag.parse("好。\n" + raw)
196
+ extracted = ge.extract(tag, now=NOW)
197
+ with tempfile.TemporaryDirectory() as tmp:
198
+ store = eg.open_store(Path(tmp) / "graph" / eg.SQLITE_FILE)
199
+ try:
200
+ ge.apply(store, extracted, now=NOW, source_entry="m9")
201
+ found = eg.neighbors(store, "医院", hops=1)
202
+ labels = {item["name"]: ge.type_label(item["type"])
203
+ for item in found["entities"]}
204
+ self.assertEqual(labels.get("小夏"), "人物")
205
+ self.assertEqual(labels.get("医院"), "地点")
206
+ self.assertEqual(len(found["edges"]), 1)
207
+ finally:
208
+ store.close()
209
+
210
+
211
+
212
+ class WiringStaticTest(unittest.TestCase):
213
+ def test_main_wires_tag_graph_ingest(self):
214
+ from pathlib import Path as _Path
215
+ source = (_Path(__file__).resolve().parents[2] / "main.py").read_text(
216
+ encoding="utf-8")
217
+ self.assertIn("_ingest_graph_from_tag", source)
218
+ self.assertIn("graph_memory_enabled", source)
219
+ self.assertIn("graph_extract as _ge", source)
220
+ self.assertIn("source_entry=str(", source)
221
+
222
+
223
+ if __name__ == "__main__":
224
+ unittest.main()