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