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.
- package/README.md +11 -0
- package/lib/core-worker.js +268 -0
- package/lib/index.js +160 -70
- package/lib/modules/autonomy.js +125 -0
- package/lib/modules/cache-stats.js +48 -0
- package/lib/modules/core-turn.js +24 -6
- package/lib/modules/expression-inject.js +0 -10
- package/lib/modules/expression-refresh.js +0 -9
- package/lib/modules/memory-capture.js +2 -1
- package/lib/modules/memory-inject.js +50 -5
- package/lib/modules/memory-tools.js +7 -4
- package/lib/modules/python-env.js +46 -0
- package/lib/modules/safety-gate.js +0 -3
- package/lib/modules/series.js +54 -0
- package/lib/modules/switch.js +16 -0
- package/lib/py-bridge.js +81 -15
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +138 -127
- package/python/biocore_memory/engine.py +2653 -2476
- package/python/biocore_memory/recall/embedding.py +129 -0
- package/python/biocore_memory/recall/feedback.py +202 -0
- package/python/biocore_model/preference_drift.py +255 -0
- package/python/biocore_model/tests/__init__.py +1 -0
- package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
- package/python/biocore_model/tests/core_status_check.mjs +29 -0
- package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
- package/python/biocore_model/tests/leak_samples_run.py +18 -0
- package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
- package/python/biocore_model/tests/panel_html_check.mjs +63 -0
- package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
- package/python/biocore_model/tests/render_guard_check.mjs +19 -0
- package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
- package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
- package/python/biocore_model/tests/test_affective_labor.py +102 -0
- package/python/biocore_model/tests/test_appraisal.py +137 -0
- package/python/biocore_model/tests/test_appraisal_action.py +55 -0
- package/python/biocore_model/tests/test_arch_convergence.py +35 -0
- package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
- package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
- package/python/biocore_model/tests/test_attachment_state.py +29 -0
- package/python/biocore_model/tests/test_attribution.py +56 -0
- package/python/biocore_model/tests/test_attributor.py +108 -0
- package/python/biocore_model/tests/test_autonomic.py +69 -0
- package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
- package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
- package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
- package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
- package/python/biocore_model/tests/test_autonomy_check.py +40 -0
- package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
- package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
- package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
- package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
- package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
- package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
- package/python/biocore_model/tests/test_autonomy_model.py +60 -0
- package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
- package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
- package/python/biocore_model/tests/test_autonomy_params.py +163 -0
- package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
- package/python/biocore_model/tests/test_autonomy_self.py +153 -0
- package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
- package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
- package/python/biocore_model/tests/test_autonomy_status.py +35 -0
- package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
- package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
- package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
- package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
- package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
- package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
- package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
- package/python/biocore_model/tests/test_bio_tick.py +202 -0
- package/python/biocore_model/tests/test_boundary.py +183 -0
- package/python/biocore_model/tests/test_boundary_gate.py +295 -0
- package/python/biocore_model/tests/test_brain_split.py +99 -0
- package/python/biocore_model/tests/test_co_recall.py +350 -0
- package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
- package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
- package/python/biocore_model/tests/test_connection_bids.py +69 -0
- package/python/biocore_model/tests/test_core_bridge.py +88 -0
- package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
- package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
- package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
- package/python/biocore_model/tests/test_core_client.py +156 -0
- package/python/biocore_model/tests/test_core_commands.py +146 -0
- package/python/biocore_model/tests/test_core_mode.py +161 -0
- package/python/biocore_model/tests/test_core_state.py +240 -0
- package/python/biocore_model/tests/test_criticality.py +61 -0
- package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
- package/python/biocore_model/tests/test_debug_values.py +169 -0
- package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
- package/python/biocore_model/tests/test_delta_budget.py +157 -0
- package/python/biocore_model/tests/test_diag_trace.py +201 -0
- package/python/biocore_model/tests/test_directives_compat.py +698 -0
- package/python/biocore_model/tests/test_dissonance.py +97 -0
- package/python/biocore_model/tests/test_drift_sign.py +87 -0
- package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
- package/python/biocore_model/tests/test_embedding_local.py +279 -0
- package/python/biocore_model/tests/test_embedding_mode.py +175 -0
- package/python/biocore_model/tests/test_embedding_modes.py +342 -0
- package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
- package/python/biocore_model/tests/test_emotion_probe.py +214 -0
- package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
- package/python/biocore_model/tests/test_entity_graph.py +198 -0
- package/python/biocore_model/tests/test_entity_resolve.py +446 -0
- package/python/biocore_model/tests/test_envy.py +173 -0
- package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
- package/python/biocore_model/tests/test_expr_guard.py +1 -0
- package/python/biocore_model/tests/test_expr_intent.py +66 -0
- package/python/biocore_model/tests/test_expr_state.py +66 -0
- package/python/biocore_model/tests/test_expr_template.py +71 -0
- package/python/biocore_model/tests/test_expr_tone.py +41 -0
- package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
- package/python/biocore_model/tests/test_feedback_flow.py +185 -0
- package/python/biocore_model/tests/test_feedback_guard.py +65 -0
- package/python/biocore_model/tests/test_feedback_llm.py +59 -0
- package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
- package/python/biocore_model/tests/test_feedback_memory.py +63 -0
- package/python/biocore_model/tests/test_feedback_user.py +61 -0
- package/python/biocore_model/tests/test_frustration_curve.py +66 -0
- package/python/biocore_model/tests/test_global_bio_state.py +116 -0
- package/python/biocore_model/tests/test_governance.py +181 -0
- package/python/biocore_model/tests/test_granularity.py +133 -0
- package/python/biocore_model/tests/test_graph_extract.py +224 -0
- package/python/biocore_model/tests/test_graph_recall.py +226 -0
- package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
- package/python/biocore_model/tests/test_growth.py +301 -0
- package/python/biocore_model/tests/test_intent_render.py +60 -0
- package/python/biocore_model/tests/test_intero_regulation.py +224 -0
- package/python/biocore_model/tests/test_intero_style.py +146 -0
- package/python/biocore_model/tests/test_item3_numeric.py +105 -0
- package/python/biocore_model/tests/test_item6_numeric.py +116 -0
- package/python/biocore_model/tests/test_layered_memory.py +174 -0
- package/python/biocore_model/tests/test_leak_guard.py +75 -0
- package/python/biocore_model/tests/test_learning_probe.py +93 -0
- package/python/biocore_model/tests/test_life_loop.py +222 -0
- package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
- package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
- package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
- package/python/biocore_model/tests/test_memory_backend.py +332 -0
- package/python/biocore_model/tests/test_memory_dedup.py +316 -0
- package/python/biocore_model/tests/test_memory_dual.py +219 -0
- package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
- package/python/biocore_model/tests/test_memory_feedback.py +241 -0
- package/python/biocore_model/tests/test_memory_fixes.py +84 -0
- package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -0
- package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
- package/python/biocore_model/tests/test_memory_store.py +372 -0
- package/python/biocore_model/tests/test_metacog.py +103 -0
- package/python/biocore_model/tests/test_migrate.py +245 -0
- package/python/biocore_model/tests/test_migrate_store.py +194 -0
- package/python/biocore_model/tests/test_mind_wander.py +65 -0
- package/python/biocore_model/tests/test_mood.py +35 -0
- package/python/biocore_model/tests/test_narrative.py +114 -0
- package/python/biocore_model/tests/test_needs_esteem.py +51 -0
- package/python/biocore_model/tests/test_needs_events.py +205 -0
- package/python/biocore_model/tests/test_needs_sleep.py +54 -0
- package/python/biocore_model/tests/test_noise_bank.py +54 -0
- package/python/biocore_model/tests/test_numeric_schema.py +98 -0
- package/python/biocore_model/tests/test_output_protocol.py +65 -0
- package/python/biocore_model/tests/test_page_debug.py +147 -0
- package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
- package/python/biocore_model/tests/test_pipeline.py +345 -0
- package/python/biocore_model/tests/test_pipeline_api.py +192 -0
- package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
- package/python/biocore_model/tests/test_plugin_config.py +85 -0
- package/python/biocore_model/tests/test_policy_learning.py +116 -0
- package/python/biocore_model/tests/test_possible_selves.py +88 -0
- package/python/biocore_model/tests/test_predictor.py +227 -0
- package/python/biocore_model/tests/test_preference_drift.py +142 -0
- package/python/biocore_model/tests/test_preset.py +142 -0
- package/python/biocore_model/tests/test_proactive_context.py +70 -0
- package/python/biocore_model/tests/test_proactive_history.py +90 -0
- package/python/biocore_model/tests/test_proactive_memory.py +141 -0
- package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
- package/python/biocore_model/tests/test_probe_contract.py +71 -0
- package/python/biocore_model/tests/test_prospective.py +154 -0
- package/python/biocore_model/tests/test_provider_fallback.py +89 -0
- package/python/biocore_model/tests/test_reconstruction.py +111 -0
- package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
- package/python/biocore_model/tests/test_relation_stage.py +114 -0
- package/python/biocore_model/tests/test_relationship_stage.py +59 -0
- package/python/biocore_model/tests/test_release_140.py +55 -0
- package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
- package/python/biocore_model/tests/test_reply_limit.py +86 -0
- package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
- package/python/biocore_model/tests/test_reply_split.py +52 -0
- package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
- package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
- package/python/biocore_model/tests/test_rerank.py +229 -0
- package/python/biocore_model/tests/test_rollback.py +175 -0
- package/python/biocore_model/tests/test_runtime.py +175 -0
- package/python/biocore_model/tests/test_runtime_active.py +205 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
- package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
- package/python/biocore_model/tests/test_scheduler.py +79 -0
- package/python/biocore_model/tests/test_secrecy.py +105 -0
- package/python/biocore_model/tests/test_self_esteem.py +45 -0
- package/python/biocore_model/tests/test_self_intent.py +347 -0
- package/python/biocore_model/tests/test_self_interest.py +183 -0
- package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
- package/python/biocore_model/tests/test_self_verification.py +113 -0
- package/python/biocore_model/tests/test_situation.py +142 -0
- package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
- package/python/biocore_model/tests/test_social_pain.py +49 -0
- package/python/biocore_model/tests/test_social_repair.py +67 -0
- package/python/biocore_model/tests/test_social_theory.py +78 -0
- package/python/biocore_model/tests/test_source_monitoring.py +204 -0
- package/python/biocore_model/tests/test_speak_like_group.py +36 -0
- package/python/biocore_model/tests/test_stages.py +226 -0
- package/python/biocore_model/tests/test_state_tag.py +86 -0
- package/python/biocore_model/tests/test_template_expr.py +65 -0
- package/python/biocore_model/tests/test_text_inference.py +70 -0
- package/python/biocore_model/tests/test_theta_params.py +115 -0
- package/python/biocore_model/tests/test_topic_control.py +200 -0
- package/python/biocore_model/tests/test_turn_memory.py +105 -0
- package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
- package/python/biocore_model/tests/test_ui_settings.py +84 -0
- package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
- package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
- package/python/biocore_model/tests/test_user_classify.py +122 -0
- package/python/biocore_model/tests/test_value_order.py +117 -0
- package/python/biocore_model/tests/test_voice_bank.py +105 -0
- package/python/biocore_model/tests/test_waiting_sense.py +216 -0
- package/python/biocore_model/tests/test_x_directives.py +101 -0
- package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
- package/python/biocore_model/tests/view_mode_check.mjs +17 -0
- package/python/biocore_runtime/autonomy.py +208 -0
- package/python/biocore_runtime/core_state.py +302 -0
- package/python/biocore_runtime/governance.py +495 -0
- package/python/biocore_runtime/growth.py +528 -0
- package/python/biocore_runtime/pipeline.py +850 -23
- package/python/biocore_runtime/relationship_state.py +214 -0
- package/python/biocore_runtime/self_layer.py +103 -0
- package/python/biocore_runtime/service.py +162 -0
- package/python/biocore_runtime/standby.py +472 -0
- package/ui/panel-layout.js +83 -39
- package/ui/panel.css +885 -743
- package/ui/panel.html +356 -296
- package/ui/panel.js +739 -865
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Wiring tests: validate the memory_dual snippets used/needed in main.py.
|
|
3
|
+
|
|
4
|
+
- sort: executes the ACTUAL snippet currently in main.py (search anchors, then
|
|
5
|
+
exec with fake scored list) and asserts ordering follows memory_dual scores.
|
|
6
|
+
- forget: executes the proposed replacement for _memory_save_doc_for_session
|
|
7
|
+
(main.py:9768-9770) with fake MEMORY_DIR/json/logger and asserts hidden marks.
|
|
8
|
+
- testing effect: executes the proposed on_recall write-back snippet.
|
|
9
|
+
- policy: reward direction, Q update direction, ui_settings persistence key.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import tempfile
|
|
13
|
+
import textwrap
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from biocore_model import memory_dual as md
|
|
18
|
+
from biocore_model import policy_learning as pl
|
|
19
|
+
from biocore_model import ui_settings
|
|
20
|
+
|
|
21
|
+
MAIN = Path(__file__).resolve().parents[2] / "main.py"
|
|
22
|
+
NOW = 1_700_000_000.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FakePlugin:
|
|
26
|
+
def __init__(self, data_dir):
|
|
27
|
+
self.data_dir = str(data_dir)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FakeSelf:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._last_memory_context = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_sort_snippet():
|
|
36
|
+
"""The real insertion in main.py: from the memory_dual import to the fallback."""
|
|
37
|
+
source = MAIN.read_text(encoding="utf-8").splitlines()
|
|
38
|
+
import_line = None
|
|
39
|
+
for index, line in enumerate(source):
|
|
40
|
+
if "from biocore_model import memory_dual as _md" in line:
|
|
41
|
+
import_line = index
|
|
42
|
+
break
|
|
43
|
+
if import_line is None:
|
|
44
|
+
raise AssertionError("memory_dual sorting snippet not found in main.py")
|
|
45
|
+
start = import_line
|
|
46
|
+
for index in range(import_line - 1, max(-1, import_line - 8), -1):
|
|
47
|
+
if source[index].strip().startswith("try:"):
|
|
48
|
+
start = index
|
|
49
|
+
break
|
|
50
|
+
if start is None:
|
|
51
|
+
raise AssertionError("memory_dual sorting snippet not found in main.py")
|
|
52
|
+
end = None
|
|
53
|
+
for index in range(start + 1, len(source)):
|
|
54
|
+
if "scored.sort(key=lambda x: x[0], reverse=True)" in source[index]:
|
|
55
|
+
end = index
|
|
56
|
+
break
|
|
57
|
+
if end is None:
|
|
58
|
+
raise AssertionError("sort fallback not found")
|
|
59
|
+
body = "\n".join(source[start:end + 1])
|
|
60
|
+
return "def _run_snippet(self, scored, now):\n" + textwrap.indent(body, " ")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DualSortWiringTest(unittest.TestCase):
|
|
64
|
+
def setUp(self):
|
|
65
|
+
self.fresh = {"id": "fresh", "text": "fresh", "storage_s": 0.6,
|
|
66
|
+
"retrieval_s": 0.55, "access_count": 0,
|
|
67
|
+
"last_access_ts": NOW - 2 * 3600, "open_loop": False}
|
|
68
|
+
self.stale = {"id": "stale", "text": "stale", "storage_s": 0.5,
|
|
69
|
+
"retrieval_s": 0.35, "access_count": 0,
|
|
70
|
+
"last_access_ts": NOW - 72 * 3600, "open_loop": False}
|
|
71
|
+
|
|
72
|
+
def test_snippet_reorders_by_dual_score(self):
|
|
73
|
+
snippet = _extract_sort_snippet()
|
|
74
|
+
scored = [(0.99, self.stale), (0.50, self.fresh)] # legacy order: stale first
|
|
75
|
+
namespace = {}
|
|
76
|
+
exec(compile(snippet, "<main.memory_dual.sort>", "exec"), namespace)
|
|
77
|
+
namespace["_run_snippet"](FakeSelf(), scored, NOW)
|
|
78
|
+
ordered = [entry["id"] for _score, entry in scored]
|
|
79
|
+
expected = sorted(
|
|
80
|
+
["fresh", "stale"],
|
|
81
|
+
key=lambda eid: -md.score(self.fresh if eid == "fresh" else self.stale,
|
|
82
|
+
NOW, context_match=1.0))
|
|
83
|
+
self.assertEqual(ordered, expected)
|
|
84
|
+
self.assertEqual(ordered[0], "fresh")
|
|
85
|
+
self.assertGreater(md.score(self.fresh, NOW), md.score(self.stale, NOW))
|
|
86
|
+
|
|
87
|
+
def test_snippet_can_fall_back(self):
|
|
88
|
+
snippet = _extract_sort_snippet()
|
|
89
|
+
scored = [(0.90, {"id": "a", "retrieval_s": 0.5, "last_access_ts": NOW}),
|
|
90
|
+
(0.10, {"id": "b", "retrieval_s": 0.5, "last_access_ts": NOW})]
|
|
91
|
+
namespace = {}
|
|
92
|
+
exec(compile(snippet, "<main.memory_dual.sort>", "exec"), namespace)
|
|
93
|
+
namespace["_run_snippet"](FakeSelf(), scored, NOW)
|
|
94
|
+
self.assertEqual([e["id"] for _s, e in scored], ["a", "b"])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
FORGET_PATCH = '''
|
|
98
|
+
def _memory_save_doc_for_session(self, path, doc):
|
|
99
|
+
try:
|
|
100
|
+
from biocore_model import memory_dual as _md
|
|
101
|
+
_rows = doc.get("entries") if isinstance(doc.get("entries"), list) else []
|
|
102
|
+
_fade = _md.forget(_rows, now=time.time())
|
|
103
|
+
doc["entries"] = _fade["entries"]
|
|
104
|
+
if _fade["forgotten_ids"]:
|
|
105
|
+
logger.info("[biocore] memory_dual hidden=%s", _fade["forgotten_ids"])
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
|
|
110
|
+
'''
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ForgetWiringTest(unittest.TestCase):
|
|
114
|
+
def test_patch_marks_hidden_without_deleting(self):
|
|
115
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
116
|
+
tmp_path = Path(tmp)
|
|
117
|
+
class Logger:
|
|
118
|
+
def info(self, *a, **k): pass
|
|
119
|
+
def warning(self, *a, **k): pass
|
|
120
|
+
namespace = {"json": json, "time": __import__("time"),
|
|
121
|
+
"logger": Logger(), "MEMORY_DIR": tmp_path}
|
|
122
|
+
exec(compile(FORGET_PATCH, "<forget_patch>", "exec"), namespace)
|
|
123
|
+
fake = object.__new__(type("Fake", (), {}))
|
|
124
|
+
import time as _time
|
|
125
|
+
now_real = _time.time()
|
|
126
|
+
doc = {"entries": [
|
|
127
|
+
{"id": "faint", "retrieval_s": 0.10, "last_access_ts": now_real - 200 * 3600},
|
|
128
|
+
{"id": "solid", "retrieval_s": 0.50, "last_access_ts": now_real},
|
|
129
|
+
]}
|
|
130
|
+
target = tmp_path / "session.json"
|
|
131
|
+
namespace["_memory_save_doc_for_session"](fake, target, doc)
|
|
132
|
+
saved = json.loads(target.read_text(encoding="utf-8"))
|
|
133
|
+
by_id = {row["id"]: row for row in saved["entries"]}
|
|
134
|
+
self.assertEqual(len(saved["entries"]), 2) # nothing deleted
|
|
135
|
+
self.assertTrue(by_id["faint"]["hidden"])
|
|
136
|
+
self.assertIn("hidden_ts", by_id["faint"])
|
|
137
|
+
self.assertNotIn("hidden", by_id["solid"])
|
|
138
|
+
|
|
139
|
+
def test_patch_second_call_is_idempotent(self):
|
|
140
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
141
|
+
tmp_path = Path(tmp)
|
|
142
|
+
class Logger:
|
|
143
|
+
def info(self, *a, **k): pass
|
|
144
|
+
def warning(self, *a, **k): pass
|
|
145
|
+
namespace = {"json": json, "time": __import__("time"),
|
|
146
|
+
"logger": Logger(), "MEMORY_DIR": tmp_path}
|
|
147
|
+
exec(compile(FORGET_PATCH, "<forget_patch>", "exec"), namespace)
|
|
148
|
+
fake = object.__new__(type("Fake", (), {}))
|
|
149
|
+
import time as _time
|
|
150
|
+
doc = {"entries": [{"id": "faint", "retrieval_s": 0.10,
|
|
151
|
+
"last_access_ts": _time.time() - 200 * 3600}]}
|
|
152
|
+
target = tmp_path / "session.json"
|
|
153
|
+
namespace["_memory_save_doc_for_session"](fake, target, doc)
|
|
154
|
+
first = json.loads(target.read_text(encoding="utf-8"))
|
|
155
|
+
namespace["_memory_save_doc_for_session"](fake, target, first)
|
|
156
|
+
second = json.loads(target.read_text(encoding="utf-8"))
|
|
157
|
+
self.assertTrue(second["entries"][0]["hidden"])
|
|
158
|
+
self.assertEqual(len(second["entries"]), 1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
TESTING_EFFECT_SNIPPET = '''
|
|
162
|
+
_upd = _md.on_recall(e, now)
|
|
163
|
+
e["storage_s"] = _upd["storage_s"]
|
|
164
|
+
e["retrieval_s"] = _upd["retrieval_s"]
|
|
165
|
+
e["access_count"] = _upd["access_count"]
|
|
166
|
+
e["last_access_ts"] = _upd["last_access_ts"]
|
|
167
|
+
'''
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestingEffectWiringTest(unittest.TestCase):
|
|
171
|
+
def test_patch_strengthens_selected_entry(self):
|
|
172
|
+
entry = {"id": "e1", "storage_s": 0.5, "retrieval_s": 0.4,
|
|
173
|
+
"access_count": 0, "last_access_ts": NOW - 24 * 3600}
|
|
174
|
+
namespace = {"e": entry, "now": NOW, "_md": md}
|
|
175
|
+
exec(compile(TESTING_EFFECT_SNIPPET, "<testing_effect>", "exec"), namespace)
|
|
176
|
+
self.assertGreater(entry["storage_s"], 0.5)
|
|
177
|
+
self.assertAlmostEqual(entry["retrieval_s"], 0.75, places=4)
|
|
178
|
+
self.assertEqual(entry["access_count"], 1)
|
|
179
|
+
self.assertEqual(entry["last_access_ts"], NOW)
|
|
180
|
+
|
|
181
|
+
def test_patch_is_idempotent_same_now(self):
|
|
182
|
+
entry = {"id": "e1", "storage_s": 0.5, "retrieval_s": 0.4,
|
|
183
|
+
"access_count": 0, "last_access_ts": 0.0}
|
|
184
|
+
namespace = {"e": entry, "now": NOW, "_md": md}
|
|
185
|
+
exec(compile(TESTING_EFFECT_SNIPPET, "<testing_effect>", "exec"), namespace)
|
|
186
|
+
first = dict(entry)
|
|
187
|
+
exec(compile(TESTING_EFFECT_SNIPPET, "<testing_effect>", "exec"), namespace)
|
|
188
|
+
self.assertEqual(entry, first)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class PolicyWiringTest(unittest.TestCase):
|
|
192
|
+
def test_reward_direction(self):
|
|
193
|
+
responded = pl.reward(True, 5.0, -0.5)
|
|
194
|
+
ignored = pl.reward(False, 900.0, 0.0)
|
|
195
|
+
self.assertGreater(responded, ignored)
|
|
196
|
+
self.assertAlmostEqual(responded, 0.541667, places=6)
|
|
197
|
+
self.assertAlmostEqual(ignored, 0.0, places=6)
|
|
198
|
+
|
|
199
|
+
def test_update_direction(self):
|
|
200
|
+
table = pl.update({}, "expect|secure|day", "approach", 1.0, alpha=0.2)
|
|
201
|
+
self.assertGreater(table["expect|secure|day"]["approach"]["q"], 0.0)
|
|
202
|
+
down = pl.update(table, "expect|secure|day", "approach", 0.0, alpha=0.2)
|
|
203
|
+
self.assertLess(down["expect|secure|day"]["approach"]["q"],
|
|
204
|
+
table["expect|secure|day"]["approach"]["q"])
|
|
205
|
+
|
|
206
|
+
def test_q_table_persists_under_ui_settings(self):
|
|
207
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
208
|
+
plugin = FakePlugin(tmp)
|
|
209
|
+
table = pl.update({}, "expect|secure|day", "share", 0.5)
|
|
210
|
+
saved = ui_settings.save(plugin, {"autonomy": {"policy_q": table}})
|
|
211
|
+
self.assertTrue(saved.get("ok"))
|
|
212
|
+
data = ui_settings.load(plugin)
|
|
213
|
+
self.assertEqual(data["autonomy"]["policy_q"],
|
|
214
|
+
table)
|
|
215
|
+
self.assertIn("policy_q", data["autonomy"])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
unittest.main()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-218: recall feedback into affect (回忆回灌情绪).
|
|
3
|
+
|
|
4
|
+
Run from the plugin root:
|
|
5
|
+
|
|
6
|
+
python -m unittest discover -s biocore_model/tests -p "test_memory_feedback.py"
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from biocore_memory.engine import MemoryEngine
|
|
15
|
+
from biocore_memory.core import config as engine_config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def entry(i, text, affect=None, importance=0.5, ts=1700000000.0):
|
|
19
|
+
value = {"id": i, "text": text, "role": "user", "ts": ts,
|
|
20
|
+
"importance": importance, "vector": [], "hits": 1,
|
|
21
|
+
"self_relevance": 0.0, "pin": False, "memory_source": "direct",
|
|
22
|
+
"memory_confidence": 0.7, "previous_versions": [],
|
|
23
|
+
"module": "biocore_memory", "scope": "default"}
|
|
24
|
+
if affect is not None:
|
|
25
|
+
value["affect"] = affect
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MemoryFeedbackTest(unittest.TestCase):
|
|
30
|
+
def setUp(self):
|
|
31
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
32
|
+
self.dir = Path(self.tmp.name)
|
|
33
|
+
self.clock = [1700000000.0]
|
|
34
|
+
self._saved = os.environ.get("BIOCORE_MEMORY_FEEDBACK")
|
|
35
|
+
os.environ.pop("BIOCORE_MEMORY_FEEDBACK", None)
|
|
36
|
+
|
|
37
|
+
def tearDown(self):
|
|
38
|
+
self.tmp.cleanup()
|
|
39
|
+
if self._saved is None:
|
|
40
|
+
os.environ.pop("BIOCORE_MEMORY_FEEDBACK", None)
|
|
41
|
+
else:
|
|
42
|
+
os.environ["BIOCORE_MEMORY_FEEDBACK"] = self._saved
|
|
43
|
+
|
|
44
|
+
def write(self, entries):
|
|
45
|
+
(self.dir / "memory.json").write_text(
|
|
46
|
+
json.dumps({"entries": entries, "tombstones": [],
|
|
47
|
+
"schema_version": 1}), encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
def engine(self, affect, enabled=True):
|
|
50
|
+
e = MemoryEngine(data_dir=str(self.dir), affect=affect,
|
|
51
|
+
now=lambda: self.clock[0])
|
|
52
|
+
if enabled:
|
|
53
|
+
e.flags["memory_feedback"] = True
|
|
54
|
+
return e
|
|
55
|
+
|
|
56
|
+
# -- switch ---------------------------------------------------------
|
|
57
|
+
def test_flag_defaults_off(self):
|
|
58
|
+
self.assertIs(engine_config.FLAGS.get("memory_feedback"), False)
|
|
59
|
+
|
|
60
|
+
def test_off_leaves_affect_unchanged(self):
|
|
61
|
+
self.write([entry("warm", "beach warm", {"valence": 0.9, "arousal": 0.9})])
|
|
62
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
63
|
+
e = self.engine(affect, enabled=False)
|
|
64
|
+
rows = e.recall("beach", budget=3)
|
|
65
|
+
self.assertEqual(len(rows), 1)
|
|
66
|
+
self.assertAlmostEqual(affect["v"], 0.0, places=6)
|
|
67
|
+
self.assertAlmostEqual(affect["a"], 0.5, places=6)
|
|
68
|
+
self.assertIsNone(e._last_memory_feedback)
|
|
69
|
+
|
|
70
|
+
# -- basic pull -----------------------------------------------------
|
|
71
|
+
def test_on_pulls_toward_recalled_affect(self):
|
|
72
|
+
self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
|
|
73
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
74
|
+
e = self.engine(affect)
|
|
75
|
+
e.recall("beach", budget=3)
|
|
76
|
+
report = e._last_memory_feedback
|
|
77
|
+
self.assertIsNotNone(report)
|
|
78
|
+
self.assertEqual(report["applied"], ["warm"])
|
|
79
|
+
self.assertAlmostEqual(report["delta"][0], 0.02, places=6)
|
|
80
|
+
self.assertAlmostEqual(report["delta"][1], 0.02, places=6)
|
|
81
|
+
self.assertAlmostEqual(affect["v"], 0.02, places=4)
|
|
82
|
+
self.assertAlmostEqual(affect["a"], 0.52, places=4)
|
|
83
|
+
|
|
84
|
+
def test_per_turn_cap_both_axes(self):
|
|
85
|
+
self.write([entry("max", "beach max", {"valence": 1.0, "arousal": 1.0})])
|
|
86
|
+
affect = {"v": -1.0, "a": 0.0}
|
|
87
|
+
e = self.engine(affect)
|
|
88
|
+
e.recall("beach", budget=3)
|
|
89
|
+
report = e._last_memory_feedback
|
|
90
|
+
self.assertTrue(report["capped"])
|
|
91
|
+
self.assertAlmostEqual(report["delta"][0], 0.02, places=6)
|
|
92
|
+
self.assertAlmostEqual(report["delta"][1], 0.02, places=6)
|
|
93
|
+
self.assertAlmostEqual(affect["v"], -0.98, places=4)
|
|
94
|
+
self.assertAlmostEqual(affect["a"], 0.02, places=4)
|
|
95
|
+
|
|
96
|
+
def test_positive_cap_is_symmetric(self):
|
|
97
|
+
self.write([entry("max", "beach max", {"valence": 1.0, "arousal": 1.0})])
|
|
98
|
+
affect = {"v": -0.2, "a": 0.3}
|
|
99
|
+
e = self.engine(affect)
|
|
100
|
+
e.recall("beach", budget=3)
|
|
101
|
+
self.assertAlmostEqual(affect["v"], -0.18, places=4)
|
|
102
|
+
self.assertAlmostEqual(affect["a"], 0.32, places=4)
|
|
103
|
+
|
|
104
|
+
def test_weighted_mean_of_batch(self):
|
|
105
|
+
self.write([
|
|
106
|
+
entry("hi", "beach one", {"valence": 0.9, "arousal": 0.9},
|
|
107
|
+
importance=1.0),
|
|
108
|
+
entry("lo", "beach two", {"valence": -0.9, "arousal": 0.1},
|
|
109
|
+
importance=0.0),
|
|
110
|
+
])
|
|
111
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
112
|
+
e = self.engine(affect)
|
|
113
|
+
e.recall("beach", budget=3)
|
|
114
|
+
report = e._last_memory_feedback
|
|
115
|
+
# weight 1.0 vs 0.0 -> target is the weighty memory (0.9, 0.9)
|
|
116
|
+
self.assertAlmostEqual(report["target"][0], 0.9, places=4)
|
|
117
|
+
self.assertAlmostEqual(report["target"][1], 0.9, places=4)
|
|
118
|
+
self.assertAlmostEqual(affect["v"], 0.02, places=4)
|
|
119
|
+
|
|
120
|
+
# -- cooldown -------------------------------------------------------
|
|
121
|
+
def test_cooldown_blocks_same_memory(self):
|
|
122
|
+
self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
|
|
123
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
124
|
+
e = self.engine(affect)
|
|
125
|
+
e.recall("beach", budget=3)
|
|
126
|
+
moved = affect["v"]
|
|
127
|
+
self.assertAlmostEqual(moved, 0.02, places=4)
|
|
128
|
+
e.recall("beach", budget=3)
|
|
129
|
+
report = e._last_memory_feedback
|
|
130
|
+
self.assertEqual(report["applied"], [])
|
|
131
|
+
self.assertEqual(report["delta"], [0.0, 0.0])
|
|
132
|
+
self.assertIn("cooldown", [x["reason"] for x in report["skipped"]])
|
|
133
|
+
self.assertAlmostEqual(affect["v"], moved, places=6)
|
|
134
|
+
|
|
135
|
+
def test_cooldown_expires_after_window(self):
|
|
136
|
+
self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
|
|
137
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
138
|
+
e = self.engine(affect)
|
|
139
|
+
e.recall("beach", budget=3)
|
|
140
|
+
self.clock[0] += engine_config.FEEDBACK_COOLDOWN + 1.0
|
|
141
|
+
e.recall("beach", budget=3)
|
|
142
|
+
report = e._last_memory_feedback
|
|
143
|
+
self.assertEqual(report["applied"], ["warm"])
|
|
144
|
+
self.assertAlmostEqual(affect["v"], 0.04, places=4)
|
|
145
|
+
|
|
146
|
+
# -- negative floor / regulation ------------------------------------
|
|
147
|
+
def test_negative_floor_blocks_descent_and_regulates(self):
|
|
148
|
+
self.write([entry("bad", "beach bad", {"valence": -0.9, "arousal": 0.5})])
|
|
149
|
+
affect = {"v": -0.5, "a": 0.5}
|
|
150
|
+
e = self.engine(affect)
|
|
151
|
+
e.recall("beach", budget=3)
|
|
152
|
+
report = e._last_memory_feedback
|
|
153
|
+
self.assertTrue(report["blocked_negative"])
|
|
154
|
+
self.assertEqual(report["delta"][0], 0.0)
|
|
155
|
+
self.assertAlmostEqual(affect["v"], -0.5, places=6)
|
|
156
|
+
regulation = report.get("regulation")
|
|
157
|
+
self.assertIsInstance(regulation, dict)
|
|
158
|
+
self.assertIn(regulation.get("strategy"),
|
|
159
|
+
("situation_selection", "situation_modification",
|
|
160
|
+
"attentional_deployment", "cognitive_reappraisal",
|
|
161
|
+
"response_modulation"))
|
|
162
|
+
self.assertEqual(report["applied"], [])
|
|
163
|
+
|
|
164
|
+
def test_negative_above_floor_still_pulls(self):
|
|
165
|
+
self.write([entry("bad", "beach bad", {"valence": -0.9, "arousal": 0.5})])
|
|
166
|
+
affect = {"v": -0.2, "a": 0.5}
|
|
167
|
+
e = self.engine(affect)
|
|
168
|
+
e.recall("beach", budget=3)
|
|
169
|
+
report = e._last_memory_feedback
|
|
170
|
+
self.assertFalse(report["blocked_negative"])
|
|
171
|
+
self.assertIsNone(report["regulation"])
|
|
172
|
+
self.assertAlmostEqual(affect["v"], -0.22, places=4)
|
|
173
|
+
|
|
174
|
+
# -- scope: only the injected top-k ---------------------------------
|
|
175
|
+
def test_only_injected_topk_matters(self):
|
|
176
|
+
self.write([
|
|
177
|
+
entry("hit", "beach picnic", {"valence": 0.9, "arousal": 0.9}),
|
|
178
|
+
entry("miss", "zebra zebra", {"valence": -1.0, "arousal": 0.0}),
|
|
179
|
+
])
|
|
180
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
181
|
+
e = self.engine(affect)
|
|
182
|
+
rows = e.recall("beach picnic", budget=1)
|
|
183
|
+
self.assertEqual(len(rows), 1)
|
|
184
|
+
self.assertEqual(rows[0]["entry"]["id"], "hit")
|
|
185
|
+
report = e._last_memory_feedback
|
|
186
|
+
self.assertEqual(report["applied"], ["hit"])
|
|
187
|
+
self.assertAlmostEqual(affect["v"], 0.02, places=4)
|
|
188
|
+
|
|
189
|
+
def test_entry_without_affect_is_skipped(self):
|
|
190
|
+
self.write([entry("plain", "beach plain")])
|
|
191
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
192
|
+
e = self.engine(affect)
|
|
193
|
+
e.recall("beach", budget=3)
|
|
194
|
+
report = e._last_memory_feedback
|
|
195
|
+
self.assertEqual(report["applied"], [])
|
|
196
|
+
self.assertEqual(report["reason"], "no_eligible_memory")
|
|
197
|
+
self.assertIn("no_affect", [x["reason"] for x in report["skipped"]])
|
|
198
|
+
self.assertAlmostEqual(affect["v"], 0.0, places=6)
|
|
199
|
+
|
|
200
|
+
# -- state-only, env switch, robustness -----------------------------
|
|
201
|
+
def test_memory_entry_is_not_rewritten(self):
|
|
202
|
+
warm = entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})
|
|
203
|
+
self.write([warm])
|
|
204
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
205
|
+
e = self.engine(affect)
|
|
206
|
+
e.recall("beach", budget=3)
|
|
207
|
+
stored = e.store.find("warm")
|
|
208
|
+
self.assertIsNotNone(stored)
|
|
209
|
+
self.assertEqual(stored["affect"], {"valence": 0.8, "arousal": 0.9})
|
|
210
|
+
self.assertEqual(len(e.store.entries), 1)
|
|
211
|
+
|
|
212
|
+
def test_env_switch_enables(self):
|
|
213
|
+
os.environ["BIOCORE_MEMORY_FEEDBACK"] = "1"
|
|
214
|
+
self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
|
|
215
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
216
|
+
e = self.engine(affect, enabled=False)
|
|
217
|
+
e.recall("beach", budget=3)
|
|
218
|
+
self.assertAlmostEqual(affect["v"], 0.02, places=4)
|
|
219
|
+
|
|
220
|
+
def test_no_affect_state_is_noop(self):
|
|
221
|
+
self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
|
|
222
|
+
e = self.engine(None)
|
|
223
|
+
rows = e.recall("beach", budget=3)
|
|
224
|
+
self.assertEqual(len(rows), 1)
|
|
225
|
+
report = e._last_memory_feedback
|
|
226
|
+
self.assertEqual(report["reason"], "no_affect_state")
|
|
227
|
+
self.assertEqual(report["applied"], [])
|
|
228
|
+
|
|
229
|
+
def test_missing_affect_dict_is_noop(self):
|
|
230
|
+
self.write([{"id": "bare", "text": "beach bare", "ts": 1700000000.0,
|
|
231
|
+
"importance": 0.5, "hits": 1, "affect": {"v": None}}])
|
|
232
|
+
affect = {"v": 0.0, "a": 0.5}
|
|
233
|
+
e = self.engine(affect)
|
|
234
|
+
e.recall("beach", budget=3)
|
|
235
|
+
report = e._last_memory_feedback
|
|
236
|
+
self.assertEqual(report["delta"], [0.0, 0.0])
|
|
237
|
+
self.assertAlmostEqual(affect["v"], 0.0, places=6)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
unittest.main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Memory fixes: _save_memory forget path, recall dedup, maintenance entries."""
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import tempfile
|
|
6
|
+
import types
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import memory_meta
|
|
11
|
+
from biocore_model import bio_tick as bt
|
|
12
|
+
|
|
13
|
+
MAIN = Path(__file__).resolve().parents[2] / "main.py"
|
|
14
|
+
SOURCE = MAIN.read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_methods(names, namespace=None):
|
|
18
|
+
tree = ast.parse(SOURCE)
|
|
19
|
+
cls = next(n for n in tree.body
|
|
20
|
+
if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
|
|
21
|
+
ns = dict(namespace or {})
|
|
22
|
+
fns = {}
|
|
23
|
+
for node in cls.body:
|
|
24
|
+
if isinstance(node, ast.FunctionDef) and node.name in names:
|
|
25
|
+
clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
|
|
26
|
+
decorator_list=[], returns=node.returns,
|
|
27
|
+
type_comment=None, lineno=node.lineno,
|
|
28
|
+
col_offset=node.col_offset,
|
|
29
|
+
end_lineno=node.end_lineno,
|
|
30
|
+
end_col_offset=node.end_col_offset)
|
|
31
|
+
exec(compile(ast.Module(body=[clone], type_ignores=[]), "<mem>", "exec"), ns)
|
|
32
|
+
fns[node.name] = ns[node.name]
|
|
33
|
+
return fns
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SaveMemoryTest(unittest.TestCase):
|
|
37
|
+
def test_save_memory_runs_forget(self):
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
fns = _load_methods(("_save_memory", "_memory_save_doc_for_session"), {
|
|
40
|
+
"json": json, "time": __import__("time"), "Path": Path,
|
|
41
|
+
"MEMORY_SCHEMA_VERSION": 2,
|
|
42
|
+
"MEMORY_DIR": Path(tmp),
|
|
43
|
+
"logger": types.SimpleNamespace(
|
|
44
|
+
warning=lambda *a, **k: None, info=lambda *a, **k: None),
|
|
45
|
+
})
|
|
46
|
+
plugin = types.SimpleNamespace(
|
|
47
|
+
_memory_path=lambda event: Path(tmp) / "s1.json")
|
|
48
|
+
plugin._save_memory = types.MethodType(fns["_save_memory"], plugin)
|
|
49
|
+
plugin._memory_save_doc_for_session = types.MethodType(
|
|
50
|
+
fns["_memory_save_doc_for_session"], plugin)
|
|
51
|
+
doc = {"entries": [{"id": "stale", "retrieval_s": 0.001,
|
|
52
|
+
"last_access_ts": 1.0, "text": "old"}]}
|
|
53
|
+
plugin._save_memory(None, doc)
|
|
54
|
+
saved = json.loads((Path(tmp) / "s1.json").read_text(encoding="utf-8"))
|
|
55
|
+
self.assertTrue(saved["entries"][0].get("hidden"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DedupTest(unittest.TestCase):
|
|
59
|
+
def test_same_cluster_keeps_two(self):
|
|
60
|
+
fns = _load_methods(("_dedup_memory_top",), {"memory_meta": memory_meta})
|
|
61
|
+
plugin = types.SimpleNamespace()
|
|
62
|
+
fn = types.MethodType(fns["_dedup_memory_top"], plugin)
|
|
63
|
+
rows = [(0.9, {"text": "今天去海边玩"}),
|
|
64
|
+
(0.8, {"text": "今天去海边玩"}),
|
|
65
|
+
(0.7, {"text": "今天去海边玩"}),
|
|
66
|
+
(0.6, {"text": "完全不同的内容"})]
|
|
67
|
+
out = fn(rows)
|
|
68
|
+
self.assertLessEqual(len(out), 3)
|
|
69
|
+
self.assertEqual(len(out), 3)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MaintenanceTest(unittest.TestCase):
|
|
73
|
+
def test_entries_are_processed_and_returned(self):
|
|
74
|
+
now = 2_000_000_000.0
|
|
75
|
+
rows = [{"id": "a", "retrieval_s": 0.9, "last_access_ts": now},
|
|
76
|
+
{"id": "b", "retrieval_s": 0.01, "last_access_ts": now}]
|
|
77
|
+
out = bt.memory_maintenance(None, now, dt_hours=1.0, entries=rows)
|
|
78
|
+
self.assertFalse(out.get("skipped"))
|
|
79
|
+
self.assertIn("entries", out)
|
|
80
|
+
self.assertEqual(len(out["entries"]), 2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
unittest.main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import random, unittest
|
|
3
|
+
from biocore_model import memory_forgetting as mf
|
|
4
|
+
|
|
5
|
+
NOW = 1_700_000_000
|
|
6
|
+
|
|
7
|
+
class ForgettingTest(unittest.TestCase):
|
|
8
|
+
def test_decay_1h(self):
|
|
9
|
+
self.assertLess(mf.strength(1.0, 1), 1.0)
|
|
10
|
+
def test_decay_monotonic(self):
|
|
11
|
+
self.assertGreater(mf.strength(1.0, 1), mf.strength(1.0, 24))
|
|
12
|
+
self.assertGreater(mf.strength(1.0, 24), mf.strength(1.0, 24 * 7))
|
|
13
|
+
def test_power_law_value(self):
|
|
14
|
+
self.assertAlmostEqual(mf.strength(1.0, 24), round(2 ** -0.5, 6), places=5)
|
|
15
|
+
def test_recall_boost(self):
|
|
16
|
+
entry = mf.recall({'strength': 0.3}, NOW)
|
|
17
|
+
self.assertGreater(entry['strength'], 0.3); self.assertEqual(entry['recall_count'], 1)
|
|
18
|
+
def test_recall_cap(self):
|
|
19
|
+
entry = mf.recall({'strength': 0.95}, NOW)
|
|
20
|
+
self.assertLessEqual(entry['strength'], 1.0)
|
|
21
|
+
def test_core_after_five(self):
|
|
22
|
+
entry = {'strength': 0.5}
|
|
23
|
+
for _ in range(6): entry = mf.recall(entry, NOW)
|
|
24
|
+
self.assertTrue(entry.get('core_memory'))
|
|
25
|
+
def test_core_never_decays(self):
|
|
26
|
+
entry = mf.decayed({'strength': 0.6, 'core_memory': True, 'last_decay_ts': NOW - 86400 * 30}, NOW)
|
|
27
|
+
self.assertEqual(entry['strength'], 0.6)
|
|
28
|
+
def test_forgotten_threshold(self):
|
|
29
|
+
self.assertFalse(mf.injectable({'strength': 0.1}))
|
|
30
|
+
self.assertTrue(mf.injectable({'strength': 0.2}))
|
|
31
|
+
def test_memory_view(self):
|
|
32
|
+
view = mf.memory_view([{'id': 'a', 'strength': 0.9}, {'id': 'b', 'strength': 0.05}])
|
|
33
|
+
self.assertEqual(view['top'][0]['id'], 'a'); self.assertEqual(view['forgotten_count'], 1)
|
|
34
|
+
def test_rumination_requires_negative(self):
|
|
35
|
+
items = mf.add_rumination([], 'message', 'm1', 'text', NOW, valence=0.2)
|
|
36
|
+
self.assertEqual(items, [])
|
|
37
|
+
def test_rumination_added(self):
|
|
38
|
+
items = mf.add_rumination([], 'message', 'm1', 'text', NOW, valence=-0.6)
|
|
39
|
+
self.assertEqual(len(items), 1); self.assertEqual(items[0]['count'], 1)
|
|
40
|
+
def test_rumination_dedupe(self):
|
|
41
|
+
items = mf.add_rumination([], 'message', 'm1', 't', NOW, conflict=True)
|
|
42
|
+
items = mf.add_rumination(items, 'message', 'm1', 't', NOW + 60, conflict=True)
|
|
43
|
+
self.assertEqual(len(items), 1); self.assertEqual(items[0]['count'], 2)
|
|
44
|
+
def test_rumination_removed_at_count5(self):
|
|
45
|
+
items = [{'target_type': 'message', 'target_id': 'm', 'count': 5, 'first_ts': NOW - 100, 'last_ts': NOW - 100, 'intensity': 1.0}]
|
|
46
|
+
self.assertEqual(mf.tick_rumination(items, NOW, random.Random(1)), [])
|
|
47
|
+
def test_rumination_removed_after_72h(self):
|
|
48
|
+
items = [{'target_type': 'message', 'target_id': 'm', 'count': 1, 'first_ts': NOW - 80 * 3600, 'last_ts': NOW - 80 * 3600}]
|
|
49
|
+
self.assertEqual(mf.tick_rumination(items, NOW, random.Random(1)), [])
|
|
50
|
+
def test_rumination_same_now_idempotent(self):
|
|
51
|
+
items = [{'target_type': 'message', 'target_id': 'm', 'count': 1, 'first_ts': NOW, 'last_ts': NOW, 'last_tick_ts': NOW}]
|
|
52
|
+
kept = mf.tick_rumination(items, NOW, random.Random(1))
|
|
53
|
+
self.assertEqual(len(kept), 1); self.assertEqual(kept[0]['count'], 1)
|
|
54
|
+
def test_rumination_text(self):
|
|
55
|
+
self.assertIn('你还在想', mf.rumination_text([{'text_snippet': '那句话'}]))
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
unittest.main()
|