dsh-biocore 0.2.0 → 0.3.1
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,29 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import autonomy_model as am
|
|
4
|
+
|
|
5
|
+
class CapsTest(unittest.TestCase):
|
|
6
|
+
def test_daily3_hourly1(self):
|
|
7
|
+
self.assertEqual(am.derive_hourly_cap(3), 1)
|
|
8
|
+
def test_daily5_hourly2(self):
|
|
9
|
+
self.assertEqual(am.derive_hourly_cap(5), 2)
|
|
10
|
+
def test_daily9_hourly3(self):
|
|
11
|
+
self.assertEqual(am.derive_hourly_cap(9), 3)
|
|
12
|
+
def test_daily20_hourly7(self):
|
|
13
|
+
self.assertEqual(am.derive_hourly_cap(20), 7)
|
|
14
|
+
def test_daily1_hourly1(self):
|
|
15
|
+
self.assertEqual(am.derive_hourly_cap(1), 1)
|
|
16
|
+
def test_clamp_daily(self):
|
|
17
|
+
self.assertEqual(am.daily_cap_value(99), 20); self.assertEqual(am.daily_cap_value(0), 1)
|
|
18
|
+
def test_default(self):
|
|
19
|
+
self.assertEqual(am.daily_cap_value(None), 5)
|
|
20
|
+
def test_effective_caps(self):
|
|
21
|
+
self.assertEqual(am.effective_caps(5), {'daily_cap': 5, 'hourly_cap': 2})
|
|
22
|
+
def test_legacy_hourly_override(self):
|
|
23
|
+
self.assertEqual(am.effective_caps(5, legacy_hourly=4)['hourly_cap'], 4)
|
|
24
|
+
def test_limit_view_has_caps(self):
|
|
25
|
+
view = am.limit_view({}, 2, 1_700_000_000, daily_cap=5)
|
|
26
|
+
self.assertEqual(view['hourly_cap'], 2); self.assertEqual(view['daily_cap'], 5)
|
|
27
|
+
|
|
28
|
+
if __name__ == '__main__':
|
|
29
|
+
unittest.main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import time, unittest
|
|
3
|
+
from biocore_model import autonomy_check as ac
|
|
4
|
+
|
|
5
|
+
BASE = {"enabled": True, "ready": True, "impulse": 0.9, "threshold": 0.6,
|
|
6
|
+
"last_sent_ts": 0, "sent_count": 0, "sent_day": "", "cooldown_seconds": 3600, "daily_cap": 3}
|
|
7
|
+
|
|
8
|
+
class AutonomyCheckTest(unittest.TestCase):
|
|
9
|
+
def test_ready(self):
|
|
10
|
+
self.assertEqual(ac.reason(dict(BASE), True, True, now=time.mktime((2026, 1, 1, 12, 0, 0, 0, 1, -1))), "ready")
|
|
11
|
+
|
|
12
|
+
def test_not_private(self):
|
|
13
|
+
self.assertEqual(ac.reason(dict(BASE), False, True), "not_private")
|
|
14
|
+
|
|
15
|
+
def test_no_session(self):
|
|
16
|
+
self.assertEqual(ac.reason(dict(BASE), True, False), "no_session")
|
|
17
|
+
|
|
18
|
+
def test_not_ready(self):
|
|
19
|
+
self.assertEqual(ac.reason(dict(BASE, ready=False)), "not_ready")
|
|
20
|
+
self.assertEqual(ac.reason(dict(BASE, impulse=0.1)), "not_ready")
|
|
21
|
+
|
|
22
|
+
def test_cooldown(self):
|
|
23
|
+
now = time.mktime((2026, 1, 1, 12, 0, 0, 0, 1, -1))
|
|
24
|
+
self.assertEqual(ac.reason(dict(BASE, last_sent_ts=now - 10), True, True, now=now), "cooldown")
|
|
25
|
+
|
|
26
|
+
def test_daily_cap(self):
|
|
27
|
+
now = time.mktime((2026, 1, 1, 12, 0, 0, 0, 1, -1))
|
|
28
|
+
day = time.strftime("%Y-%m-%d", time.localtime(now))
|
|
29
|
+
self.assertEqual(ac.reason(dict(BASE, sent_day=day, sent_count=3), True, True, now=now), "daily_cap")
|
|
30
|
+
|
|
31
|
+
def test_snapshot_fields(self):
|
|
32
|
+
snap = ac.snapshot(dict(BASE, impulse_ema=0.5, max_chars=120, last_sent_ts=1.0))
|
|
33
|
+
self.assertIn("impulse", snap); self.assertIn("max_chars", snap)
|
|
34
|
+
|
|
35
|
+
def test_chinese_mapping(self):
|
|
36
|
+
for code in ("ready", "not_ready", "cooldown", "daily_cap", "quiet_hours", "no_session", "not_private"):
|
|
37
|
+
self.assertTrue(ac.reason_text(code))
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
unittest.main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import random, unittest
|
|
3
|
+
from biocore_model import autonomy_model as am
|
|
4
|
+
|
|
5
|
+
NOW = 1_700_000_000
|
|
6
|
+
|
|
7
|
+
class ClingyTest(unittest.TestCase):
|
|
8
|
+
def test_withdraw_clingy(self):
|
|
9
|
+
self.assertAlmostEqual(am.silence_phase(30, True)['drive_mult'], 0.90)
|
|
10
|
+
def test_withdraw_fallback(self):
|
|
11
|
+
self.assertAlmostEqual(am.silence_phase(30, False)['drive_mult'], 0.35)
|
|
12
|
+
def test_other_phases(self):
|
|
13
|
+
self.assertAlmostEqual(am.silence_phase(1, True)['drive_mult'], 1.00)
|
|
14
|
+
self.assertAlmostEqual(am.silence_phase(3, True)['drive_mult'], 1.20)
|
|
15
|
+
self.assertAlmostEqual(am.silence_phase(10, True)['drive_mult'], 0.80)
|
|
16
|
+
def test_retry_allowed(self):
|
|
17
|
+
self.assertTrue(am.withdraw_retry_allowed(NOW - 7 * 3600, NOW))
|
|
18
|
+
def test_retry_blocked(self):
|
|
19
|
+
self.assertFalse(am.withdraw_retry_allowed(NOW - 2 * 3600, NOW))
|
|
20
|
+
def test_burst_probability(self):
|
|
21
|
+
rng = random.Random(0)
|
|
22
|
+
hits = sum(1 for _ in range(2000) if am.clingy_burst(rng))
|
|
23
|
+
self.assertGreater(hits / 2000, 0.10); self.assertLess(hits / 2000, 0.30)
|
|
24
|
+
def test_burst_zero(self):
|
|
25
|
+
self.assertFalse(am.clingy_burst(random.Random(1), p=0.0))
|
|
26
|
+
def test_burst_one(self):
|
|
27
|
+
self.assertTrue(am.clingy_burst(random.Random(1), p=1.0))
|
|
28
|
+
def test_view_multiplier_on_burst(self):
|
|
29
|
+
view = am.silence_view(NOW - 30 * 3600, 0, NOW, clingy=True, rng=random.Random(0))
|
|
30
|
+
if view['burst']:
|
|
31
|
+
self.assertAlmostEqual(view['effective_mult'], round(view['drive_mult'] * 1.5, 4))
|
|
32
|
+
def test_view_fields(self):
|
|
33
|
+
view = am.silence_view(NOW - 3 * 3600, 0, NOW, clingy=True)
|
|
34
|
+
for key in ('hours', 'phase', 'drive_mult', 'burst', 'clingy', 'effective_mult'):
|
|
35
|
+
self.assertIn(key, view)
|
|
36
|
+
def test_status_keeps_clingy(self):
|
|
37
|
+
snap = am.status_snapshot({'silence': {'phase': 'withdraw', 'clingy': True, 'burst': True}})
|
|
38
|
+
self.assertTrue(snap['silence']['clingy'])
|
|
39
|
+
|
|
40
|
+
if __name__ == '__main__':
|
|
41
|
+
unittest.main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Cooldown/daily-cap gating + post-send self-contact loneliness damping."""
|
|
3
|
+
import ast
|
|
4
|
+
import time
|
|
5
|
+
import types
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_model import autonomy_model as am
|
|
10
|
+
from biocore_model import autonomy_tick as at
|
|
11
|
+
|
|
12
|
+
MAIN = Path(__file__).resolve().parents[2] / "main.py"
|
|
13
|
+
NOW = 1_800_000_000.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load(name):
|
|
17
|
+
tree = ast.parse(MAIN.read_text(encoding="utf-8"))
|
|
18
|
+
cls = next(n for n in tree.body
|
|
19
|
+
if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
|
|
20
|
+
node = next(f for f in cls.body
|
|
21
|
+
if isinstance(f, ast.FunctionDef) and f.name == name)
|
|
22
|
+
ns = {"time": time}
|
|
23
|
+
exec(compile(ast.Module(body=[node], type_ignores=[]), "<a>", "exec"), ns)
|
|
24
|
+
return ns[name]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParamsTest(unittest.TestCase):
|
|
28
|
+
def test_apply_forces_cooldown_and_cap(self):
|
|
29
|
+
fn = _load("_apply_autonomy_params")
|
|
30
|
+
plugin = types.SimpleNamespace()
|
|
31
|
+
out = fn(plugin, {}, {"theta": 0.52, "cooldown_s": 1625,
|
|
32
|
+
"daily_cap": 3})
|
|
33
|
+
self.assertEqual(out["cooldown_seconds"], 1625)
|
|
34
|
+
self.assertEqual(out["daily_cap"], 3)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GateTest(unittest.TestCase):
|
|
38
|
+
def test_cooldown_rejects_five_minutes(self):
|
|
39
|
+
fn = _load("_autonomy_gate_limits")
|
|
40
|
+
ok, why = fn(types.SimpleNamespace(), {"last_sent_ts": NOW - 300},
|
|
41
|
+
NOW, cooldown_seconds=1625)
|
|
42
|
+
self.assertFalse(ok)
|
|
43
|
+
self.assertEqual(why, "cooldown")
|
|
44
|
+
|
|
45
|
+
def test_cooldown_allows_after_30_minutes(self):
|
|
46
|
+
fn = _load("_autonomy_gate_limits")
|
|
47
|
+
ok, why = fn(types.SimpleNamespace(), {"last_sent_ts": NOW - 2000},
|
|
48
|
+
NOW, cooldown_seconds=1625)
|
|
49
|
+
self.assertTrue(ok, why)
|
|
50
|
+
|
|
51
|
+
def test_daily_cap_blocks(self):
|
|
52
|
+
fn = _load("_autonomy_gate_limits")
|
|
53
|
+
day = time.strftime("%Y-%m-%d", time.localtime(NOW))
|
|
54
|
+
ok, why = fn(types.SimpleNamespace(), {
|
|
55
|
+
"sent_day": day, "sent_count": 3}, NOW, daily_cap=3)
|
|
56
|
+
self.assertFalse(ok)
|
|
57
|
+
self.assertEqual(why, "daily_cap")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Core:
|
|
61
|
+
def snapshot(self):
|
|
62
|
+
return {"a": 0.4, "energy": 0.5, "stress": 0.2}
|
|
63
|
+
|
|
64
|
+
labels = {"emotion": "neutral", "intensity": 0.0}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PartsTest(unittest.TestCase):
|
|
68
|
+
def test_self_contact_resets_gap(self):
|
|
69
|
+
acc = {"last_user_msg_ts": NOW - 10 * 3600,
|
|
70
|
+
"last_self_contact_ts": NOW - 100}
|
|
71
|
+
parts = am.parts_from_state(Core(), {}, now=NOW, acc=acc,
|
|
72
|
+
params={"silence_tau_hours": 2.0,
|
|
73
|
+
"social_tau_hours": 18.0})
|
|
74
|
+
self.assertLess(parts["lonely"], 0.2)
|
|
75
|
+
|
|
76
|
+
def test_lonely_damped_within_30_minutes(self):
|
|
77
|
+
base = am.parts_from_state(Core(), {}, now=NOW,
|
|
78
|
+
acc={"last_user_msg_ts": NOW - 5 * 3600,
|
|
79
|
+
"last_self_contact_ts": NOW - 3600},
|
|
80
|
+
params={"silence_tau_hours": 2.0})
|
|
81
|
+
damped = am.parts_from_state(Core(), {}, now=NOW,
|
|
82
|
+
acc={"last_user_msg_ts": NOW - 5 * 3600,
|
|
83
|
+
"last_self_contact_ts": NOW - 1},
|
|
84
|
+
params={"silence_tau_hours": 2.0})
|
|
85
|
+
self.assertLess(damped["lonely"], base["lonely"])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SendBookkeepingTest(unittest.TestCase):
|
|
89
|
+
def test_send_records_self_contact_and_sent_count(self):
|
|
90
|
+
class Deps:
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.acc = {"V": 1.0, "theta": 0.55, "pending_fire": True}
|
|
93
|
+
self.sent = []
|
|
94
|
+
|
|
95
|
+
def load_acc(self):
|
|
96
|
+
return dict(self.acc)
|
|
97
|
+
|
|
98
|
+
def save_acc(self, acc):
|
|
99
|
+
self.acc = dict(acc)
|
|
100
|
+
|
|
101
|
+
def list_sessions(self):
|
|
102
|
+
return [{"umo": "u1"}]
|
|
103
|
+
|
|
104
|
+
def get_core(self, umo):
|
|
105
|
+
return Core()
|
|
106
|
+
|
|
107
|
+
def any_core(self):
|
|
108
|
+
return Core()
|
|
109
|
+
|
|
110
|
+
def snapshot(self, core):
|
|
111
|
+
return {}
|
|
112
|
+
|
|
113
|
+
def params(self):
|
|
114
|
+
return {"theta": 0.55}
|
|
115
|
+
|
|
116
|
+
def step(self, V, drift, theta):
|
|
117
|
+
return {"V": V, "fire": True}
|
|
118
|
+
|
|
119
|
+
def gate(self, core, acc, silence, now):
|
|
120
|
+
return True, "fired"
|
|
121
|
+
|
|
122
|
+
def build_text(self, core, acc):
|
|
123
|
+
return "在。"
|
|
124
|
+
|
|
125
|
+
def send_text(self, umo, text):
|
|
126
|
+
self.sent.append(umo)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
deps = Deps()
|
|
130
|
+
out = at.tick_once(deps, NOW)
|
|
131
|
+
self.assertEqual(out["sent"], ["u1"])
|
|
132
|
+
self.assertEqual(deps.acc["last_self_contact_ts"], NOW)
|
|
133
|
+
self.assertEqual(deps.acc["V"], 0.0)
|
|
134
|
+
self.assertEqual(deps.acc["sent_count"], 1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
unittest.main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import autonomy_model as am
|
|
4
|
+
|
|
5
|
+
NOW = 1_700_000_000
|
|
6
|
+
|
|
7
|
+
class FrustrationTest(unittest.TestCase):
|
|
8
|
+
def test_frustration_increments(self):
|
|
9
|
+
out = am.block_feedback({'frustration': 0}, NOW)
|
|
10
|
+
self.assertEqual(out['acc']['frustration'], 1); self.assertAlmostEqual(out['drive_mult'], 1.04)
|
|
11
|
+
def test_drive_mult_after_four(self):
|
|
12
|
+
out = am.block_feedback({'frustration': 3}, NOW)
|
|
13
|
+
self.assertAlmostEqual(out['drive_mult'], 1.16)
|
|
14
|
+
def test_drive_mult_cap_130(self):
|
|
15
|
+
out = am.block_feedback({'frustration': 10}, NOW)
|
|
16
|
+
self.assertAlmostEqual(out['drive_mult'], 1.30)
|
|
17
|
+
def test_cap_after_12_blocks(self):
|
|
18
|
+
acc = {}
|
|
19
|
+
for _ in range(12):
|
|
20
|
+
out = am.block_feedback(acc, NOW)
|
|
21
|
+
acc = out['acc']
|
|
22
|
+
self.assertAlmostEqual(out['drive_mult'], 1.30)
|
|
23
|
+
def test_emotion_directions(self):
|
|
24
|
+
out = am.block_feedback({}, NOW)
|
|
25
|
+
self.assertGreater(out['stress'], 0); self.assertGreater(out['arousal'], 0); self.assertEqual(out['valence'], 0.0) # v7: limits only touch stress/arousal
|
|
26
|
+
def test_no_v_decay_field(self):
|
|
27
|
+
self.assertNotIn('drive_scale', am.block_feedback({}, NOW))
|
|
28
|
+
def test_clear_on_success(self):
|
|
29
|
+
acc = am.clear_frustration({'frustration': 5, 'drive_mult': 1.15})
|
|
30
|
+
self.assertEqual(acc['frustration'], 0); self.assertAlmostEqual(acc['drive_mult'], 1.0)
|
|
31
|
+
def test_window_reset_clears_frustration(self):
|
|
32
|
+
acc = am.hourly_window({'hour_window_start': NOW - 3700, 'frustration': 4, 'drive_mult': 1.12}, NOW)
|
|
33
|
+
self.assertEqual(acc['frustration'], 0)
|
|
34
|
+
def test_limit_view_fields(self):
|
|
35
|
+
view = am.limit_view({'frustration': 4, 'drive_mult': 1.12}, 3, NOW)
|
|
36
|
+
self.assertEqual(view['frustration'], 4); self.assertAlmostEqual(view['drive_mult'], 1.12)
|
|
37
|
+
def test_counters_still_count(self):
|
|
38
|
+
out = am.block_feedback({'blocked_count': 6}, NOW)
|
|
39
|
+
self.assertEqual(out['total_blocked'], 7); self.assertEqual(out['acc']['hour_blocked_count'], 1)
|
|
40
|
+
|
|
41
|
+
if __name__ == '__main__':
|
|
42
|
+
unittest.main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import time, unittest
|
|
3
|
+
from biocore_model import autonomy_model as am
|
|
4
|
+
|
|
5
|
+
NOW = 1_700_000_000
|
|
6
|
+
|
|
7
|
+
class HourlyLimitTest(unittest.TestCase):
|
|
8
|
+
def test_under_cap_allowed(self):
|
|
9
|
+
ok, acc, cap = am.hourly_allowed({'hour_sent_count': 2}, NOW, 3)
|
|
10
|
+
self.assertTrue(ok); self.assertEqual(cap, 3)
|
|
11
|
+
def test_cap_reached_blocked(self):
|
|
12
|
+
ok, _, _ = am.hourly_allowed({'hour_sent_count': 3}, NOW, 3)
|
|
13
|
+
self.assertFalse(ok)
|
|
14
|
+
def test_window_roll_resets(self):
|
|
15
|
+
acc = am.hourly_window({'hour_window_start': NOW - 3700, 'hour_sent_count': 3, 'hour_blocked_count': 2}, NOW)
|
|
16
|
+
self.assertEqual(acc['hour_sent_count'], 0); self.assertEqual(acc['hour_blocked_count'], 0)
|
|
17
|
+
def test_window_not_reset_within_hour(self):
|
|
18
|
+
acc = am.hourly_window({'hour_window_start': NOW - 100, 'hour_sent_count': 2}, NOW)
|
|
19
|
+
self.assertEqual(acc['hour_sent_count'], 2)
|
|
20
|
+
def test_block_feedback_counters(self):
|
|
21
|
+
out = am.block_feedback({'blocked_count': 6}, NOW)
|
|
22
|
+
self.assertEqual(out['total_blocked'], 7); self.assertEqual(out['acc']['hour_blocked_count'], 1)
|
|
23
|
+
def test_block_feedback_emotion_deltas(self):
|
|
24
|
+
out = am.block_feedback({}, NOW)
|
|
25
|
+
self.assertGreater(out['stress'], 0); self.assertEqual(out['valence'], 0.0); self.assertGreater(out['drive_mult'], 1.0)
|
|
26
|
+
def test_clingy_retry_always_allowed(self):
|
|
27
|
+
self.assertTrue(am.withdraw_retry_allowed(NOW - 60, NOW, clingy=True))
|
|
28
|
+
def test_non_clingy_retry_waits_6h(self):
|
|
29
|
+
self.assertFalse(am.withdraw_retry_allowed(NOW - 60, NOW, clingy=False))
|
|
30
|
+
self.assertTrue(am.withdraw_retry_allowed(NOW - 7 * 3600, NOW, clingy=False))
|
|
31
|
+
def test_limit_view_fields(self):
|
|
32
|
+
view = am.limit_view({'hour_sent_count': 2, 'hour_blocked_count': 5, 'blocked_count': 9}, 3, NOW)
|
|
33
|
+
for key in ('hourly_cap', 'hour_sent', 'hour_blocked', 'total_blocked', 'window_reset_in'):
|
|
34
|
+
self.assertIn(key, view)
|
|
35
|
+
def test_limit_view_values(self):
|
|
36
|
+
view = am.limit_view({'hour_sent_count': 2, 'hour_blocked_count': 5, 'blocked_count': 9}, 3, NOW)
|
|
37
|
+
self.assertEqual(view['hour_sent'], 2); self.assertEqual(view['hour_blocked'], 5); self.assertEqual(view['total_blocked'], 9)
|
|
38
|
+
def test_cap_clamped(self):
|
|
39
|
+
ok, _, cap = am.hourly_allowed({}, NOW, 99)
|
|
40
|
+
self.assertEqual(cap, 12)
|
|
41
|
+
def test_status_snapshot_includes_limit(self):
|
|
42
|
+
snap = am.status_snapshot({'limit': {'hour_sent': 1, 'hourly_cap': 3}})
|
|
43
|
+
self.assertIn('limit', snap)
|
|
44
|
+
|
|
45
|
+
if __name__ == '__main__':
|
|
46
|
+
unittest.main()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import autonomy_loop as loop
|
|
4
|
+
|
|
5
|
+
NOW = 1_700_000_000
|
|
6
|
+
|
|
7
|
+
class LoopTest(unittest.TestCase):
|
|
8
|
+
def test_record_attempt(self):
|
|
9
|
+
acc = loop.record_attempt({}, NOW, 'lonely', 0.4, 12)
|
|
10
|
+
self.assertEqual(acc['sent_count'], 1); self.assertEqual(acc['last_attempt']['code'], 'lonely')
|
|
11
|
+
def test_quick_reply_counts(self):
|
|
12
|
+
acc = loop.record_attempt({}, NOW, 'lonely', 0.4, 10)
|
|
13
|
+
acc = loop.on_user_reply(acc, NOW + 600)
|
|
14
|
+
self.assertEqual(acc['responded_count'], 1); self.assertAlmostEqual(acc['last_response_latency_s'], 600.0)
|
|
15
|
+
def test_late_reply_not_counted(self):
|
|
16
|
+
acc = loop.record_attempt({}, NOW)
|
|
17
|
+
acc = loop.on_user_reply(acc, NOW + 7 * 3600)
|
|
18
|
+
self.assertEqual(acc.get('responded_count', 0), 0)
|
|
19
|
+
def test_settle_ignored(self):
|
|
20
|
+
acc = loop.record_attempt({}, NOW)
|
|
21
|
+
acc = loop.settle(acc, NOW + 7 * 3600)
|
|
22
|
+
self.assertEqual(acc['ignored_count'], 1); self.assertTrue(acc['last_attempt']['closed'])
|
|
23
|
+
def test_settle_recent_not_ignored(self):
|
|
24
|
+
acc = loop.record_attempt({}, NOW)
|
|
25
|
+
acc = loop.settle(acc, NOW + 60)
|
|
26
|
+
self.assertEqual(acc.get('ignored_count', 0), 0)
|
|
27
|
+
def test_response_rate(self):
|
|
28
|
+
self.assertAlmostEqual(loop.response_rate({'responded_count': 5, 'ignored_count': 7}), round(5/12, 4))
|
|
29
|
+
def test_rate_empty(self):
|
|
30
|
+
self.assertEqual(loop.response_rate({}), 0.0)
|
|
31
|
+
def test_per_code_ewma_up(self):
|
|
32
|
+
learning = loop.per_code_stats({}, 'lonely', True)
|
|
33
|
+
self.assertGreater(learning['per_code']['lonely']['rate'], 0.5)
|
|
34
|
+
def test_per_code_ewma_down(self):
|
|
35
|
+
learning = loop.per_code_stats({}, 'lonely', False)
|
|
36
|
+
self.assertLess(learning['per_code']['lonely']['rate'], 0.5)
|
|
37
|
+
def test_tune_every_ten(self):
|
|
38
|
+
learning = {'per_code': {'social_gap': {'sent': 10, 'rate': 0.8}}, 'last_tune_ts': NOW - 10}
|
|
39
|
+
weights = {'social_gap': 0.5, 'lonely': 0.5}
|
|
40
|
+
new, learning2 = loop.tune_weights(learning, weights, NOW, total_sent=10)
|
|
41
|
+
self.assertNotEqual(new['social_gap'], 0.5)
|
|
42
|
+
self.assertAlmostEqual(sum(new.values()), 1.0, places=6)
|
|
43
|
+
self.assertLessEqual(abs(new['social_gap'] - 0.5), 0.05 + 1e-9)
|
|
44
|
+
def test_tune_step_cap(self):
|
|
45
|
+
learning = {'per_code': {'social_gap': {'sent': 10, 'rate': 1.0}}, 'last_tune_ts': NOW - 10}
|
|
46
|
+
old = {'social_gap': 0.5, 'lonely': 0.5}
|
|
47
|
+
new, _ = loop.tune_weights(learning, old, NOW, total_sent=10)
|
|
48
|
+
self.assertGreater(new['social_gap'], 0.5); self.assertLessEqual(new['social_gap'], 0.55)
|
|
49
|
+
def test_tune_not_due(self):
|
|
50
|
+
learning = {'per_code': {'x': {'sent': 1, 'rate': 0.9}}, 'last_tune_ts': NOW - 10}
|
|
51
|
+
weights = {'x': 1.0}
|
|
52
|
+
new, _ = loop.tune_weights(learning, weights, NOW, total_sent=3)
|
|
53
|
+
self.assertEqual(new, weights)
|
|
54
|
+
def test_loop_view_fields(self):
|
|
55
|
+
view = loop.loop_view({'sent_count': 12, 'responded_count': 5, 'ignored_count': 7, 'last_attempt': {'code': 'lonely', 'ts': NOW}}, {'per_code': {'lonely': {'sent': 3, 'rate': 0.4}}})
|
|
56
|
+
for key in ('sent', 'responded', 'ignored', 'response_rate', 'avg_latency_s', 'last_attempt', 'per_code'):
|
|
57
|
+
self.assertIn(key, view)
|
|
58
|
+
def test_stall_detection(self):
|
|
59
|
+
self.assertTrue(loop.tick_stalled({'last_tick_ts': NOW - 700}, NOW, 300))
|
|
60
|
+
self.assertFalse(loop.tick_stalled({'last_tick_ts': NOW - 100}, NOW, 300))
|
|
61
|
+
self.assertFalse(loop.tick_stalled({}, NOW))
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
unittest.main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import autonomy_mode
|
|
4
|
+
|
|
5
|
+
class FakePlugin:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.config = {"proactive_autonomy": False, "proactive_send": False,
|
|
8
|
+
"proactive_autonomy_threshold": 0.6, "proactive_autonomy_max_chars": 120}
|
|
9
|
+
self.saved = []
|
|
10
|
+
async def _persist_plugin_config(self, updates):
|
|
11
|
+
self.saved.append(dict(updates)); self.config.update(updates)
|
|
12
|
+
|
|
13
|
+
class AutonomyRouteTest(unittest.IsolatedAsyncioTestCase):
|
|
14
|
+
async def test_valid_save(self):
|
|
15
|
+
plugin = FakePlugin()
|
|
16
|
+
result = await autonomy_mode.apply(plugin, {"enabled": True, "send": True, "threshold": 0.7, "max_chars": 200})
|
|
17
|
+
self.assertTrue(result["ok"]); self.assertEqual(plugin.saved[-1]["proactive_autonomy"], True)
|
|
18
|
+
self.assertEqual(result["values"]["threshold"], 0.7); self.assertEqual(result["values"]["max_chars"], 200)
|
|
19
|
+
async def test_invalid_threshold(self):
|
|
20
|
+
with self.assertRaises(autonomy_mode.AutonomyError) as caught:
|
|
21
|
+
await autonomy_mode.apply(FakePlugin(), {"threshold": 1.7})
|
|
22
|
+
self.assertEqual(caught.exception.status, 400)
|
|
23
|
+
async def test_invalid_max_chars(self):
|
|
24
|
+
with self.assertRaises(autonomy_mode.AutonomyError):
|
|
25
|
+
await autonomy_mode.apply(FakePlugin(), {"max_chars": 5})
|
|
26
|
+
async def test_bad_bool_type(self):
|
|
27
|
+
with self.assertRaises(autonomy_mode.AutonomyError):
|
|
28
|
+
await autonomy_mode.apply(FakePlugin(), {"enabled": "yes"})
|
|
29
|
+
async def test_partial_update_only(self):
|
|
30
|
+
plugin = FakePlugin()
|
|
31
|
+
await autonomy_mode.apply(plugin, {"send": True})
|
|
32
|
+
self.assertEqual(list(plugin.saved[-1].keys()), ["proactive_send"])
|
|
33
|
+
async def test_returns_current(self):
|
|
34
|
+
plugin = FakePlugin()
|
|
35
|
+
result = await autonomy_mode.apply(plugin, {})
|
|
36
|
+
self.assertEqual(result["values"]["threshold"], 0.6)
|
|
37
|
+
if __name__ == "__main__": unittest.main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import math, unittest
|
|
3
|
+
from biocore_model import autonomy_model as am
|
|
4
|
+
|
|
5
|
+
class AutonomyModelTest(unittest.TestCase):
|
|
6
|
+
def test_gap_zero_near_zero(self):
|
|
7
|
+
self.assertLess(am.social_gap(0, 18, 1.0, 1.0), 0.01)
|
|
8
|
+
|
|
9
|
+
def test_gap_12h_about_half(self):
|
|
10
|
+
self.assertAlmostEqual(am.social_gap(12, 18, 1.0, 1.0), 0.487, delta=0.05)
|
|
11
|
+
|
|
12
|
+
def test_gap_48h_about_09(self):
|
|
13
|
+
value = am.social_gap(48, 18, 1.0, 1.0) * (0.6 + 0.4 * 1.0)
|
|
14
|
+
self.assertGreater(value, 0.85)
|
|
15
|
+
|
|
16
|
+
def test_closeness_scales_gap(self):
|
|
17
|
+
self.assertGreater(am.social_gap(24, 18, 1.0, 1.0), am.social_gap(24, 18, 1.0, 0.0))
|
|
18
|
+
|
|
19
|
+
def test_lonely_multiplies_prob_intensity(self):
|
|
20
|
+
self.assertAlmostEqual(am.lonely(0.8, 0.5), 0.4)
|
|
21
|
+
|
|
22
|
+
def test_quiet_raises_cost(self):
|
|
23
|
+
quiet = am.gate(0.8, 0.6, 0.5, 0.35, quiet=True)
|
|
24
|
+
normal = am.gate(0.8, 0.6, 0.5, 0.35, quiet=False)
|
|
25
|
+
self.assertGreater(quiet['cost'], normal['cost'])
|
|
26
|
+
|
|
27
|
+
def test_recent_activity_lowers_cost(self):
|
|
28
|
+
active = am.gate(0.8, 0.6, 0.5, 0.35, recent_user_active=True)
|
|
29
|
+
idle = am.gate(0.8, 0.6, 0.5, 0.35, recent_user_active=False)
|
|
30
|
+
self.assertLess(active['cost'], idle['cost'])
|
|
31
|
+
|
|
32
|
+
def test_lonely_discounts_cost(self):
|
|
33
|
+
high = am.gate(0.8, 0.6, 0.5, 0.35, lonely_value=1.0)
|
|
34
|
+
low = am.gate(0.8, 0.6, 0.5, 0.35, lonely_value=0.0)
|
|
35
|
+
self.assertLess(high['cost'], low['cost'])
|
|
36
|
+
|
|
37
|
+
def test_habit_power(self):
|
|
38
|
+
self.assertAlmostEqual(am.habit(1.0, 0), 1.0)
|
|
39
|
+
self.assertAlmostEqual(am.habit(1.0, 1), 0.7)
|
|
40
|
+
self.assertAlmostEqual(am.habit(1.0, 2), 0.49)
|
|
41
|
+
|
|
42
|
+
def test_compute_fields(self):
|
|
43
|
+
out = am.compute({'social_gap': 0.5, 'lonely': 0.2, 'stress_load': 0.1, 'drive_dev': 0.1, 'echo': 0.0, 'noise': 0.2}, 0.6)
|
|
44
|
+
for key in ('drive', 'parts', 'gate', 'ready'):
|
|
45
|
+
self.assertIn(key, out)
|
|
46
|
+
for key in ('social_gap', 'lonely', 'anxiety_fear', 'sadness', 'joy', 'anger_conflict', 'boredom', 'stress_load', 'drive_dev'):
|
|
47
|
+
self.assertIn(key, out['parts'])
|
|
48
|
+
for key in ('p_ready', 'benefit', 'cost', 'expected'):
|
|
49
|
+
self.assertIn(key, out['gate'])
|
|
50
|
+
|
|
51
|
+
def test_weights_normalised(self):
|
|
52
|
+
out = am.compute({'social_gap': 1.0, 'lonely': 1.0, 'stress_load': 1.0, 'drive_dev': 1.0, 'echo': 1.0, 'noise': 1.0}, 0.6)
|
|
53
|
+
self.assertLessEqual(out['drive'], 1.0)
|
|
54
|
+
|
|
55
|
+
def test_ready_requires_gate_and_threshold(self):
|
|
56
|
+
out = am.compute({'social_gap': 0.0, 'lonely': 0.0, 'stress_load': 0.0, 'drive_dev': 0.0, 'echo': 0.0, 'noise': 0.0}, 0.6)
|
|
57
|
+
self.assertFalse(out['ready'])
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
unittest.main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import autonomy_model as am
|
|
4
|
+
|
|
5
|
+
BASE = {'social_gap': 0.3, 'lonely': 0.1, 'anxiety_fear': 0.1, 'sadness': 0.1, 'joy': 0.1,
|
|
6
|
+
'anger_conflict': 0.1, 'boredom': 0.1, 'stress_load': 0.1, 'drive_dev': 0.1}
|
|
7
|
+
|
|
8
|
+
def d(parts): return am.drive(parts)
|
|
9
|
+
|
|
10
|
+
class AutonomyV2Test(unittest.TestCase):
|
|
11
|
+
def test_weight_sum_is_one(self):
|
|
12
|
+
total = sum(am.DEFAULTS[k] for k in am.WEIGHT_KEYS)
|
|
13
|
+
self.assertAlmostEqual(total, 1.0, places=6)
|
|
14
|
+
|
|
15
|
+
def test_anxiety_raises_drive(self):
|
|
16
|
+
self.assertGreater(d(dict(BASE, anxiety_fear=0.9)), d(BASE))
|
|
17
|
+
|
|
18
|
+
def test_sadness_raises_drive(self):
|
|
19
|
+
self.assertGreater(d(dict(BASE, sadness=0.9)), d(BASE))
|
|
20
|
+
|
|
21
|
+
def test_joy_raises_drive(self):
|
|
22
|
+
self.assertGreater(d(dict(BASE, joy=0.9)), d(BASE))
|
|
23
|
+
|
|
24
|
+
def test_anger_raises_drive(self):
|
|
25
|
+
self.assertGreater(d(dict(BASE, anger_conflict=0.9)), d(BASE))
|
|
26
|
+
|
|
27
|
+
def test_boredom_raises_drive(self):
|
|
28
|
+
self.assertGreater(d(dict(BASE, boredom=0.9)), d(BASE))
|
|
29
|
+
|
|
30
|
+
def test_anger_raises_cost(self):
|
|
31
|
+
high = am.gate(0.8, 0.6, 0.5, anger=1.0)
|
|
32
|
+
low = am.gate(0.8, 0.6, 0.5, anger=0.0)
|
|
33
|
+
self.assertGreater(high['cost'], low['cost'])
|
|
34
|
+
|
|
35
|
+
def test_joy_lowers_cost(self):
|
|
36
|
+
high = am.gate(0.8, 0.6, 0.5, joy=1.0)
|
|
37
|
+
low = am.gate(0.8, 0.6, 0.5, joy=0.0)
|
|
38
|
+
self.assertLess(high['cost'], low['cost'])
|
|
39
|
+
|
|
40
|
+
def test_anxiety_lowers_cost(self):
|
|
41
|
+
self.assertLess(am.gate(0.8, 0.6, 0.5, anxiety=1.0)['cost'], am.gate(0.8, 0.6, 0.5, anxiety=0.0)['cost'])
|
|
42
|
+
|
|
43
|
+
def test_missing_emotions_no_crash(self):
|
|
44
|
+
out = am.compute({}, 0.6)
|
|
45
|
+
self.assertEqual(out['drive'], 0.0)
|
|
46
|
+
self.assertIn('anxiety_fear', out['parts'])
|
|
47
|
+
|
|
48
|
+
def test_parts_complete(self):
|
|
49
|
+
out = am.compute(BASE, 0.6)
|
|
50
|
+
for key in ('social_gap', 'lonely', 'anxiety_fear', 'sadness', 'joy', 'anger_conflict', 'boredom', 'stress_load', 'drive_dev'):
|
|
51
|
+
self.assertIn(key, out['parts'])
|
|
52
|
+
|
|
53
|
+
def test_gate_fields(self):
|
|
54
|
+
out = am.compute(BASE, 0.6)
|
|
55
|
+
for key in ('p_ready', 'benefit', 'cost', 'expected'):
|
|
56
|
+
self.assertIn(key, out['gate'])
|
|
57
|
+
|
|
58
|
+
def test_habit_still_applies(self):
|
|
59
|
+
self.assertAlmostEqual(am.habit(1.0, 2), 0.49, places=6)
|
|
60
|
+
|
|
61
|
+
def test_missing_weights_fall_back(self):
|
|
62
|
+
self.assertGreaterEqual(am.drive(dict(BASE, joy=1.0), {'w_joy': 0.5}), 0.0)
|
|
63
|
+
|
|
64
|
+
def test_ready_false_without_gate(self):
|
|
65
|
+
self.assertFalse(am.compute({}, 0.6)['ready'])
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
unittest.main()
|