dsh-biocore 0.3.3 → 0.5.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/lib/index.js +30 -1
- package/lib/modules/standby.js +61 -0
- package/lib/modules/state.js +22 -3
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +7 -3
- package/python/biocore_memory/engine.py +91 -6
- package/python/biocore_memory/recall/embedding.py +0 -51
- package/python/biocore_memory/recall/feedback.py +34 -7
- package/python/biocore_memory/recall/fusion.py +5 -1
- package/python/biocore_memory/scoring.py +203 -0
- package/python/biocore_memory/space.py +355 -0
- package/python/biocore_memory/store/dedupe.py +1 -0
- package/python/biocore_memory/store/tiering.py +14 -3
- package/python/biocore_model/affective_labor.py +14 -1
- package/python/biocore_model/appraisal.py +172 -3
- package/python/biocore_model/bio_tick.py +43 -13
- package/python/biocore_model/emotion_geometry.py +9 -0
- package/python/biocore_model/envy.py +21 -0
- package/python/biocore_model/feedback_memory.py +53 -0
- package/python/biocore_model/needs_events.py +32 -11
- package/python/biocore_model/regulation_strategies.py +5 -0
- package/python/biocore_model/repair_after_boundary.py +1 -0
- package/python/biocore_model/self_esteem.py +14 -2
- package/python/biocore_model/self_intent.py +6 -4
- package/python/biocore_model/tests/test_autonomy_self.py +153 -153
- package/python/biocore_model/tests/test_cache_series.py +137 -0
- package/python/biocore_model/tests/test_core_state.py +240 -240
- package/python/biocore_model/tests/test_extensions.py +167 -167
- package/python/biocore_model/tests/test_field_arbiter.py +444 -0
- package/python/biocore_model/tests/test_governance.py +181 -181
- package/python/biocore_model/tests/test_lifecycle.py +153 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -211
- package/python/biocore_model/tests/test_retrieval_plus.py +143 -0
- package/python/biocore_model/tests/test_signals.py +106 -106
- package/python/biocore_model/tests/test_standby.py +199 -0
- package/python/biocore_runtime/__init__.py +3 -0
- package/python/biocore_runtime/arbiter.py +307 -0
- package/python/biocore_runtime/batch_b.py +724 -0
- package/python/biocore_runtime/batch_c.py +186 -0
- package/python/biocore_runtime/batch_d.py +659 -0
- package/python/biocore_runtime/batch_e.py +328 -0
- package/python/biocore_runtime/batch_registry.py +446 -0
- package/python/biocore_runtime/cache_stats.py +161 -0
- package/python/biocore_runtime/core_state.py +106 -3
- package/python/biocore_runtime/dimensions.py +419 -0
- package/python/biocore_runtime/expression.py +261 -0
- package/python/biocore_runtime/field.py +440 -0
- package/python/biocore_runtime/lifecycle.py +233 -0
- package/python/biocore_runtime/perception.py +1045 -0
- package/python/biocore_runtime/pipeline.py +4458 -2974
- package/python/biocore_runtime/priority.py +210 -0
- package/python/biocore_runtime/retrieval_plus.py +352 -0
- package/python/biocore_runtime/standby.py +581 -472
- package/python/biocore_runtime/timescales.py +263 -0
- package/python/data/signals.json +3266 -206
- package/ui/panel-layout.js +32 -29
- package/ui/panel.css +563 -974
- package/ui/panel.html +427 -401
- package/ui/panel.js +543 -450
|
@@ -26,8 +26,15 @@ import re
|
|
|
26
26
|
|
|
27
27
|
DIMS = ("goal", "blame", "control", "certainty", "novelty", "involve", "hit")
|
|
28
28
|
OUTPUTS = ("v", "a", "d", "social", "trust", "stress", "self_esteem")
|
|
29
|
+
# [engineering] per-turn output caps, hand-tuned.
|
|
29
30
|
CAPS = {"v": 0.15, "a": 0.15, "d": 0.10, "social": 0.05,
|
|
30
31
|
"trust": 0.05, "stress": 0.10, "self_esteem": 0.05}
|
|
32
|
+
# appraisal_v2 (default ON): v is goal + an explicit valence signal only.
|
|
33
|
+
# blame goes to trust/conflict, hit to stress/self_esteem, and control /
|
|
34
|
+
# certainty / novelty / involve each keep their own output line. The old 7x7
|
|
35
|
+
# mapping stays reachable with BIOCORE_APPRAISAL_V2=0 or flags["appraisal_v2"].
|
|
36
|
+
SWITCH_KEY = "appraisal_v2"
|
|
37
|
+
ENV_KEY = "BIOCORE_APPRAISAL_V2"
|
|
31
38
|
PROBE_MIN_CONFIDENCE = 0.4
|
|
32
39
|
WHY_MAX = 40
|
|
33
40
|
INVOLVE_GAIN = 0.5
|
|
@@ -37,7 +44,9 @@ INVOLVE_GAIN = 0.5
|
|
|
37
44
|
FACT_MAX_CHARS = 20
|
|
38
45
|
SITUATION_KEYS = ("challenge", "praise", "hurt")
|
|
39
46
|
|
|
40
|
-
# 7 x 7 W matrix: WEIGHTS[dimension][output] = weight.
|
|
47
|
+
# 7 x 7 W matrix: WEIGHTS[dimension][output] = weight. [engineering] the
|
|
48
|
+
# dimension names follow Scherer 2001 / OCC, but no row below is fitted or
|
|
49
|
+
# transcribed from a paper (see PAPER-MAP.md).
|
|
41
50
|
# goal: -1 blocked .. +1 achieved -> better valence/trust, less stress.
|
|
42
51
|
# blame: 0 self .. 1 other -> conflict, lower self-esteem/trust, more stress.
|
|
43
52
|
# control: 0 powerless .. 1 in control -> dominance/self-esteem, less stress.
|
|
@@ -64,6 +73,38 @@ WEIGHTS = {
|
|
|
64
73
|
}
|
|
65
74
|
DEFAULT_WEIGHTS = {"%s.%s" % (dim, out): weight
|
|
66
75
|
for dim, row in WEIGHTS.items() for out, weight in row.items()}
|
|
76
|
+
|
|
77
|
+
# v2 destination table. Every dimension has exactly one home (goal and the
|
|
78
|
+
# explicit valence signal own v); nothing is folded into v a second time.
|
|
79
|
+
V2_WEIGHTS = {
|
|
80
|
+
"v": {"goal": 0.60, "valence": 0.40},
|
|
81
|
+
"a": {"novelty": 0.25, "involve": 0.10, "hit": 0.20},
|
|
82
|
+
"d": {"control": 0.45, "goal": 0.20},
|
|
83
|
+
"social": {"involve": 0.20, "hit": -0.30},
|
|
84
|
+
"trust": {"goal": 0.15, "blame": -0.30},
|
|
85
|
+
"conflict": {"blame": 0.35, "hit": 0.10},
|
|
86
|
+
"stress": {"blame": 0.15, "hit": 0.30},
|
|
87
|
+
"self_esteem": {"goal": 0.20, "hit": -0.30},
|
|
88
|
+
}
|
|
89
|
+
V2_OUTPUTS = ("v", "a", "d", "social", "trust", "conflict", "stress",
|
|
90
|
+
"self_esteem")
|
|
91
|
+
V2_CAPS = dict(CAPS, conflict=0.15)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def enabled(flags=None, env=None):
|
|
95
|
+
"""appraisal_v2: explicit flag > nested switches > env > default ON."""
|
|
96
|
+
import os
|
|
97
|
+
data = flags if isinstance(flags, dict) else {}
|
|
98
|
+
if isinstance(data.get(SWITCH_KEY), bool):
|
|
99
|
+
return data[SWITCH_KEY]
|
|
100
|
+
switches = data.get("switches") if isinstance(data.get("switches"), dict) else {}
|
|
101
|
+
if isinstance(switches.get(SWITCH_KEY), bool):
|
|
102
|
+
return switches[SWITCH_KEY]
|
|
103
|
+
environ = env if hasattr(env, "get") else os.environ
|
|
104
|
+
raw = str(environ.get(ENV_KEY) or "").strip().lower()
|
|
105
|
+
if raw in ("0", "false", "off", "no"):
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
67
108
|
_FEEDBACK_FIELDS = {"v": "valence_delta", "a": "arousal_delta",
|
|
68
109
|
"d": "dominance_delta", "social": "social_delta",
|
|
69
110
|
"trust": "trust_delta", "stress": "stress_delta",
|
|
@@ -182,8 +223,62 @@ async def probe(text, context, llm_call, timeout=None, core=None, now=None):
|
|
|
182
223
|
return {}
|
|
183
224
|
|
|
184
225
|
|
|
185
|
-
def
|
|
186
|
-
"""
|
|
226
|
+
def _v2_values(dims):
|
|
227
|
+
"""Inputs for the v2 table: the 7 dims plus an optional explicit valence."""
|
|
228
|
+
dims = dims if isinstance(dims, dict) else {}
|
|
229
|
+
values = {}
|
|
230
|
+
for name in DIMS:
|
|
231
|
+
low = -1.0 if name == "goal" else 0.0
|
|
232
|
+
values[name] = _clamp(dims.get(name, 0.0), low, 1.0)
|
|
233
|
+
values["valence"] = _clamp(dims.get("valence", 0.0), -1.0, 1.0)
|
|
234
|
+
return values
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _appraise_v2(dims, confidence=0.5, weights=None):
|
|
238
|
+
"""v2 mapping: (deltas, components). Nothing is added into v twice."""
|
|
239
|
+
conf = _clamp(confidence, 0.0, 1.0)
|
|
240
|
+
values = _v2_values(dims)
|
|
241
|
+
table = {out: dict(row) for out, row in V2_WEIGHTS.items()}
|
|
242
|
+
if isinstance(weights, dict):
|
|
243
|
+
for key, value in weights.items():
|
|
244
|
+
if "." not in key:
|
|
245
|
+
continue
|
|
246
|
+
dim, output = key.split(".", 1)
|
|
247
|
+
number = _num(value)
|
|
248
|
+
if dim in values and output in table and number is not None:
|
|
249
|
+
table[output][dim] = number
|
|
250
|
+
deltas = {}
|
|
251
|
+
components = {}
|
|
252
|
+
for output in V2_OUTPUTS:
|
|
253
|
+
rows = {}
|
|
254
|
+
total = 0.0
|
|
255
|
+
for name, weight in (table.get(output) or {}).items():
|
|
256
|
+
part = float(weight) * values.get(name, 0.0) * conf
|
|
257
|
+
if abs(part) > 1e-9:
|
|
258
|
+
rows[name] = round(part, 6)
|
|
259
|
+
total += part
|
|
260
|
+
cap = float(V2_CAPS.get(output, 0.2))
|
|
261
|
+
capped = max(-cap, min(cap, total))
|
|
262
|
+
components[output] = {"rows": rows, "total": round(total, 6),
|
|
263
|
+
"capped": round(capped, 6),
|
|
264
|
+
"dropped": round(total - capped, 6)}
|
|
265
|
+
if abs(capped) > 1e-9:
|
|
266
|
+
deltas[output] = round(capped, 4)
|
|
267
|
+
return deltas, components
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def appraise(dims, confidence=0.5, weights=None, v2=None):
|
|
271
|
+
"""W·x scaled by confidence, bounded by per-output caps.
|
|
272
|
+
|
|
273
|
+
v2 (default ON) uses V2_WEIGHTS: v = goal + explicit valence only, blame
|
|
274
|
+
and hit go to their own outputs. v2=False is the old 7x7 mapping, byte for
|
|
275
|
+
byte.
|
|
276
|
+
"""
|
|
277
|
+
if v2 is None:
|
|
278
|
+
v2 = enabled()
|
|
279
|
+
if v2:
|
|
280
|
+
deltas, _components = _appraise_v2(dims, confidence, weights)
|
|
281
|
+
return deltas
|
|
187
282
|
dims = dims if isinstance(dims, dict) else {}
|
|
188
283
|
conf = _clamp(confidence, 0.0, 1.0)
|
|
189
284
|
w = {dim: dict(row) for dim, row in WEIGHTS.items()}
|
|
@@ -214,6 +309,80 @@ def appraise(dims, confidence=0.5, weights=None):
|
|
|
214
309
|
return out
|
|
215
310
|
|
|
216
311
|
|
|
312
|
+
def appraise_detail(dims, confidence=0.5, weights=None, v2=None):
|
|
313
|
+
"""Same numbers as appraise(), plus where each one came from.
|
|
314
|
+
|
|
315
|
+
components[output][dim] is that dim's own contribution, so v/a are visible
|
|
316
|
+
derived readouts instead of a black box that quietly folds blame/hit in.
|
|
317
|
+
Nothing is merged: the caller can show every source line.
|
|
318
|
+
"""
|
|
319
|
+
if v2 is None:
|
|
320
|
+
v2 = enabled()
|
|
321
|
+
if v2:
|
|
322
|
+
deltas, components = _appraise_v2(dims, confidence, weights)
|
|
323
|
+
return {"deltas": deltas, "components": components,
|
|
324
|
+
"mapping": "v2",
|
|
325
|
+
"derived": {"v": True, "a": False}}
|
|
326
|
+
dims = dims if isinstance(dims, dict) else {}
|
|
327
|
+
conf = _clamp(confidence, 0.0, 1.0)
|
|
328
|
+
w = {dim: dict(row) for dim, row in WEIGHTS.items()}
|
|
329
|
+
if isinstance(weights, dict):
|
|
330
|
+
for key, value in weights.items():
|
|
331
|
+
if "." not in key:
|
|
332
|
+
continue
|
|
333
|
+
dim, output = key.split(".", 1)
|
|
334
|
+
number = _num(value)
|
|
335
|
+
if dim in w and output in w[dim] and number is not None:
|
|
336
|
+
w[dim][output] = number
|
|
337
|
+
values = {}
|
|
338
|
+
for name in DIMS:
|
|
339
|
+
low = -1.0 if name == "goal" else 0.0
|
|
340
|
+
values[name] = _clamp(dims.get(name, 0.0), low, 1.0)
|
|
341
|
+
gain = 1.0 + INVOLVE_GAIN * values["involve"]
|
|
342
|
+
components = {}
|
|
343
|
+
deltas = {}
|
|
344
|
+
for output in OUTPUTS:
|
|
345
|
+
rows = {}
|
|
346
|
+
total = 0.0
|
|
347
|
+
for name in DIMS:
|
|
348
|
+
part = float(w[name].get(output, 0.0)) * values[name] * gain * conf
|
|
349
|
+
if abs(part) > 1e-9:
|
|
350
|
+
rows[name] = round(part, 6)
|
|
351
|
+
total += part
|
|
352
|
+
cap = float(CAPS[output])
|
|
353
|
+
capped = max(-cap, min(cap, total))
|
|
354
|
+
components[output] = {"rows": rows,
|
|
355
|
+
"total": round(total, 6),
|
|
356
|
+
"capped": round(capped, 6),
|
|
357
|
+
"dropped": round(total - capped, 6)}
|
|
358
|
+
if abs(capped) > 1e-9:
|
|
359
|
+
deltas[output] = round(capped, 4)
|
|
360
|
+
return {"deltas": deltas, "components": components,
|
|
361
|
+
"mapping": "v1", "derived": {key: True for key in ("v", "a")}}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def legacy_v(dims, confidence=0.5, weights=None):
|
|
365
|
+
"""The old 7x7 v (v1), kept for comparison only -- never written back."""
|
|
366
|
+
out = appraise(dims, confidence=confidence, weights=weights, v2=False)
|
|
367
|
+
return out.get("v")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def appraise_with_legacy(dims, confidence=0.5, weights=None, v2=None):
|
|
371
|
+
"""{deltas, v, v_legacy, mapping} for /state.appraisal.
|
|
372
|
+
|
|
373
|
+
With v2 on, v_legacy is what the old mapping would have produced, so the
|
|
374
|
+
two can be compared without changing anything. With v2 off there is no
|
|
375
|
+
second mapping and v_legacy is None.
|
|
376
|
+
"""
|
|
377
|
+
if v2 is None:
|
|
378
|
+
v2 = enabled()
|
|
379
|
+
deltas = appraise(dims, confidence=confidence, weights=weights, v2=v2)
|
|
380
|
+
return {"deltas": deltas,
|
|
381
|
+
"v": deltas.get("v", 0.0),
|
|
382
|
+
"v_legacy": legacy_v(dims, confidence, weights) if v2 else None,
|
|
383
|
+
"mapping": "v2" if v2 else "v1"}
|
|
384
|
+
|
|
385
|
+
|
|
217
386
|
def appraise_dims(dims, weights=None):
|
|
218
387
|
"""Compatibility: full-confidence mapping (no confidence scaling)."""
|
|
219
388
|
return appraise(dims, confidence=1.0, weights=weights)
|
|
@@ -10,7 +10,8 @@ Literature
|
|
|
10
10
|
Craig 2009 (insula interoception), Barrett 2017 (constructed emotion),
|
|
11
11
|
Seth 2013 (interoceptive prediction), Garfinkel 2015 (three dimensions),
|
|
12
12
|
Sterling 2012 (allostasis), Carver & Scheier 1998 (cybernetic control),
|
|
13
|
-
Powers 1973 (perceptual control), Gross 1998 (regulation strategies)
|
|
13
|
+
Powers 1973 (perceptual control), Gross 1998 (regulation strategies),
|
|
14
|
+
Anderson & Schooler 1991 (power-law forgetting, ACT-R activation).
|
|
14
15
|
|
|
15
16
|
Hard constraints: pure functions, stdlib only, no main import, no network, no
|
|
16
17
|
model call; the clock is injected (now / dt_hours); repeated calls with the same
|
|
@@ -32,9 +33,12 @@ DT_CAP_HOURS = 6.0
|
|
|
32
33
|
DISCOMFORT_TRIGGER = 0.35
|
|
33
34
|
LOG_EVERY = 10
|
|
34
35
|
LOG_ENV = "BIOCORE_BIO_TICK_LOG"
|
|
35
|
-
DEFAULT_VALENCE_DECAY_MINUTES = 120.0
|
|
36
|
-
DEFAULT_AROUSAL_DECAY_MINUTES = 30.0
|
|
37
|
-
DEFAULT_SOCIAL_DECAY_MINUTES = 180.0
|
|
36
|
+
DEFAULT_VALENCE_DECAY_MINUTES = 120.0 # [engineering] hand-tuned
|
|
37
|
+
DEFAULT_AROUSAL_DECAY_MINUTES = 30.0 # [engineering] hand-tuned
|
|
38
|
+
DEFAULT_SOCIAL_DECAY_MINUTES = 180.0 # [engineering] hand-tuned
|
|
39
|
+
# Anderson & Schooler 1991, Psychological Science 2:396-408 (ACT-R activation):
|
|
40
|
+
# forgetting follows a power law, not an exponential; d ~= 0.5 in their fits.
|
|
41
|
+
POWER_LAW_EXPONENT = 0.5
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
def _num(value, default=0.0):
|
|
@@ -78,12 +82,36 @@ def _snapshot(core):
|
|
|
78
82
|
return {}
|
|
79
83
|
|
|
80
84
|
|
|
81
|
-
def
|
|
85
|
+
def _power_law_factor(minutes, half_life, exponent=None):
|
|
86
|
+
"""Anderson & Schooler 1991 power law of forgetting: (1 + t / tau) ** -d.
|
|
87
|
+
|
|
88
|
+
tau is solved from the configured half-life (tau = half_life / (2**(1/d)-1))
|
|
89
|
+
so a caller's half-life still means a half-life, while the tail decays like
|
|
90
|
+
an activation trace instead of dropping geometrically. d is configurable
|
|
91
|
+
per core through the decay_exponent attribute.
|
|
92
|
+
"""
|
|
82
93
|
try:
|
|
83
94
|
hl = max(1.0, float(half_life))
|
|
84
95
|
except Exception:
|
|
85
96
|
hl = 120.0
|
|
86
|
-
|
|
97
|
+
try:
|
|
98
|
+
power = POWER_LAW_EXPONENT if exponent is None else float(exponent)
|
|
99
|
+
except Exception:
|
|
100
|
+
power = POWER_LAW_EXPONENT
|
|
101
|
+
d = max(0.05, min(2.0, power))
|
|
102
|
+
try:
|
|
103
|
+
span = max(1e-6, 2.0 ** (1.0 / d) - 1.0)
|
|
104
|
+
except Exception:
|
|
105
|
+
span = 3.0
|
|
106
|
+
tau = hl / span
|
|
107
|
+
try:
|
|
108
|
+
minutes = max(0.0, float(minutes))
|
|
109
|
+
except Exception:
|
|
110
|
+
minutes = 0.0
|
|
111
|
+
return (1.0 + minutes / tau) ** (-d)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_half_life_factor = _power_law_factor # older callers keep working
|
|
87
115
|
|
|
88
116
|
|
|
89
117
|
def _derive(core):
|
|
@@ -147,13 +175,15 @@ def _decay(core, dt_hours):
|
|
|
147
175
|
v_rec = recovery if v < 0 else 1.0
|
|
148
176
|
a_rec = recovery if a > 0.40 else 1.0
|
|
149
177
|
s_rec = recovery if soc < 0.55 else 1.0
|
|
150
|
-
|
|
151
|
-
_put(core, "
|
|
152
|
-
minutes,
|
|
153
|
-
_put(core, "
|
|
154
|
-
minutes,
|
|
155
|
-
_put(core, "
|
|
156
|
-
minutes,
|
|
178
|
+
power = _get(core, "decay_exponent", POWER_LAW_EXPONENT)
|
|
179
|
+
_put(core, "v", _clip(v * _power_law_factor(
|
|
180
|
+
minutes, v_hl * emo_factor / v_rec, power), -1, 1))
|
|
181
|
+
_put(core, "a", _clip(0.40 + (a - 0.40) * _power_law_factor(
|
|
182
|
+
minutes, a_hl * max(0.5, emo_factor) / a_rec, power), 0, 1))
|
|
183
|
+
_put(core, "d", _clip(0.55 + (d - 0.55) * _power_law_factor(
|
|
184
|
+
minutes, v_hl * emo_factor, power), -1, 1))
|
|
185
|
+
_put(core, "social", _clip(0.55 + (soc - 0.55) * _power_law_factor(
|
|
186
|
+
minutes, s_hl * emo_factor / s_rec, power), 0, 1))
|
|
157
187
|
_derive(core)
|
|
158
188
|
return True
|
|
159
189
|
|
|
@@ -23,6 +23,8 @@ PROTOTYPES = {
|
|
|
23
23
|
"loneliness": {"v": -0.5, "a": -0.4, "d": -0.3, "social": -0.7, "stress": 0.2},
|
|
24
24
|
"trust": {"v": 0.4, "a": 0.1, "d": 0.2, "social": 0.5, "trust": 0.7},
|
|
25
25
|
}
|
|
26
|
+
# [engineering] six-axis weights and the distance formula have no source;
|
|
27
|
+
# the axis choice echoes Russell 1980 / Mehrabian 1996 (see PAPER-MAP.md).
|
|
26
28
|
WEIGHTS = {"v": 0.35, "a": 0.25, "d": 0.15, "social": 0.15,
|
|
27
29
|
"stress": 0.10, "trust": 0.15}
|
|
28
30
|
CORE_DIMS = ("v", "a", "d", "social", "stress", "trust")
|
|
@@ -90,7 +92,14 @@ def classify(state, temperature=0.25):
|
|
|
90
92
|
coverage = present_weight / total_weight if total_weight > 0 else 1.0
|
|
91
93
|
# Missing dimensions always reduce confidence; neutral fill is not evidence.
|
|
92
94
|
confidence *= max(0.0, min(1.0, coverage))
|
|
95
|
+
# Raw per-emotion closeness (1 - distance) stays visible; the softmax
|
|
96
|
+
# probability is a DISPLAY label only, never fed back into a decision.
|
|
97
|
+
scores = {name: round(max(0.0, 1.0 - distance), 4)
|
|
98
|
+
for name, distance in distances.items()}
|
|
93
99
|
return {"label": label,
|
|
100
|
+
"scores": scores,
|
|
101
|
+
"distances": {name: round(value, 4)
|
|
102
|
+
for name, value in distances.items()},
|
|
94
103
|
"probs": {name: round(prob, 4) for name, prob in probs.items()},
|
|
95
104
|
"confidence": round(max(0.0, min(1.0, confidence)), 4),
|
|
96
105
|
"top2": top2,
|
|
@@ -113,6 +113,26 @@ def _normalize(signals):
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
def _components(norm):
|
|
117
|
+
"""Every signal behind the intensity, each on its own line.
|
|
118
|
+
|
|
119
|
+
intensity is kept as a display readout; the components are the evidence,
|
|
120
|
+
so "comparison" and "self-threat" are never fused into one number.
|
|
121
|
+
"""
|
|
122
|
+
norm = norm if isinstance(norm, dict) else {}
|
|
123
|
+
closeness = _clamp01(norm.get("closeness", 0.5), 0.5)
|
|
124
|
+
fairness = _clamp01(norm.get("fairness", 0.5), 0.5)
|
|
125
|
+
third = _clamp01(norm.get("third_party", 0.0), 0.0)
|
|
126
|
+
self_eval = _clamp01(norm.get("self_evaluation", 0.5), 0.5)
|
|
127
|
+
return {
|
|
128
|
+
"comparison": round(third, 4),
|
|
129
|
+
"relative_deprivation": round(1.0 - fairness, 4),
|
|
130
|
+
"self_threat": round(1.0 - self_eval, 4),
|
|
131
|
+
"closeness": round(closeness, 4),
|
|
132
|
+
"presence": round(third * (0.5 + 0.5 * closeness), 4),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
116
136
|
def intensity_of(signals):
|
|
117
137
|
"""0..1 comparison intensity; the single number behind the three levels."""
|
|
118
138
|
norm = _normalize(signals)
|
|
@@ -163,6 +183,7 @@ def assess(signals, config=None):
|
|
|
163
183
|
"behavior": "",
|
|
164
184
|
"reason": REASONS[""],
|
|
165
185
|
"signals": norm,
|
|
186
|
+
"components": _components(norm),
|
|
166
187
|
}
|
|
167
188
|
if not out["enabled"]:
|
|
168
189
|
out["reason"] = REASONS["disabled"]
|
|
@@ -14,6 +14,7 @@ BOOST = 0.25
|
|
|
14
14
|
CORE_AFTER = 5
|
|
15
15
|
D = 0.5
|
|
16
16
|
TAU_HOURS = 24.0
|
|
17
|
+
COOLDOWN_SECONDS = 3600.0 # [engineering] one boost per memory per hour
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _num(value, default=0.0):
|
|
@@ -50,6 +51,58 @@ def touch(entries, event_id, now, kind="recall"):
|
|
|
50
51
|
return {"entries": items, "deltas": {}, "audit": [], "idempotent": False}
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def touch_batch(entries, event_ids, now, kind="recall", limit=None):
|
|
55
|
+
"""Touch several recalled memories: one row per entry, never one aggregate.
|
|
56
|
+
|
|
57
|
+
Each entry keeps its own old/new/boost row; an entry already touched at or
|
|
58
|
+
after `now` is skipped (cooldown), and `limit` caps how many may move in
|
|
59
|
+
one turn. Returns {"entries", "rows", "deltas", "skipped"}.
|
|
60
|
+
"""
|
|
61
|
+
items = [dict(entry or {}) for entry in (entries or [])]
|
|
62
|
+
wanted = [str(item) for item in (event_ids or []) if str(item or "")]
|
|
63
|
+
if limit is None:
|
|
64
|
+
limit = len(wanted) or 0
|
|
65
|
+
rows = []
|
|
66
|
+
skipped = []
|
|
67
|
+
moved = 0
|
|
68
|
+
for target in wanted:
|
|
69
|
+
if moved >= max(0, int(limit)):
|
|
70
|
+
skipped.append({"id": target, "reason": "turn_cap"})
|
|
71
|
+
continue
|
|
72
|
+
found = None
|
|
73
|
+
for entry in items:
|
|
74
|
+
if str(entry.get("id")) == target:
|
|
75
|
+
found = entry
|
|
76
|
+
break
|
|
77
|
+
if found is None:
|
|
78
|
+
skipped.append({"id": target, "reason": "not_found"})
|
|
79
|
+
continue
|
|
80
|
+
last_touch = float(found.get("last_touch_ts", 0.0) or 0.0)
|
|
81
|
+
if last_touch > 0 and float(now) - last_touch < COOLDOWN_SECONDS:
|
|
82
|
+
skipped.append({"id": target, "reason": "cooldown"})
|
|
83
|
+
continue
|
|
84
|
+
old = _num(found.get("strength"), 1.0)
|
|
85
|
+
found["strength"] = round(min(1.0, old + BOOST), 4)
|
|
86
|
+
found["recall_count"] = int(found.get("recall_count", 0) or 0) + 1
|
|
87
|
+
found["last_touched"] = float(now)
|
|
88
|
+
found["last_decay_ts"] = float(now)
|
|
89
|
+
found["last_touch_ts"] = float(now)
|
|
90
|
+
found["last_touch_kind"] = str(kind)
|
|
91
|
+
if found["recall_count"] > CORE_AFTER:
|
|
92
|
+
found["core_memory"] = True
|
|
93
|
+
rows.append({"id": target, "old": round(old, 4),
|
|
94
|
+
"new": found["strength"],
|
|
95
|
+
"boost": round(found["strength"] - old, 4),
|
|
96
|
+
"recall_count": found["recall_count"],
|
|
97
|
+
"source": "feedback_memory", "reason": str(kind)})
|
|
98
|
+
moved += 1
|
|
99
|
+
deltas = {row["id"]: {"strength": row["new"],
|
|
100
|
+
"recall_count": row["recall_count"]}
|
|
101
|
+
for row in rows}
|
|
102
|
+
return {"entries": items, "rows": rows, "deltas": deltas,
|
|
103
|
+
"skipped": skipped, "idempotent": not rows}
|
|
104
|
+
|
|
105
|
+
|
|
53
106
|
def decay_all(entries, now, d=D, tau_hours=TAU_HOURS):
|
|
54
107
|
items = []
|
|
55
108
|
forgotten = []
|
|
@@ -108,30 +108,51 @@ def apply(needs, events, scale=1.0):
|
|
|
108
108
|
"events": applied, "scale": round(factor, 3)}
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def
|
|
112
|
-
"""
|
|
111
|
+
def need_biases(needs, baseline=BASELINE):
|
|
112
|
+
"""One row per need: value / deficit / surplus / bias.
|
|
113
|
+
|
|
114
|
+
Each need keeps its own bias; nothing is combined into one number, so
|
|
115
|
+
reading belonging never tells you anything about control.
|
|
116
|
+
"""
|
|
113
117
|
state = needs_state(needs)
|
|
114
118
|
base = _clamp(baseline, 0.0, 1.0)
|
|
119
|
+
rows = {}
|
|
120
|
+
for key in NEED_KEYS:
|
|
121
|
+
deficit = max(0.0, base - state[key])
|
|
122
|
+
surplus = max(0.0, state[key] - base)
|
|
123
|
+
rows[key] = {"value": state[key],
|
|
124
|
+
"deficit": round(deficit, 4),
|
|
125
|
+
"surplus": round(surplus, 4),
|
|
126
|
+
"bias": round(surplus - deficit, 4)}
|
|
127
|
+
return rows
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def behavior_bias(needs, baseline=BASELINE):
|
|
131
|
+
"""Action inclinations, each read from exactly ONE need (no weighted mix).
|
|
132
|
+
|
|
133
|
+
Before this change self_assert summed self_esteem and control, and
|
|
134
|
+
ask_clarify summed control and meaningful_existence; a deficit in one need
|
|
135
|
+
could therefore masquerade as a deficit in another.
|
|
136
|
+
"""
|
|
137
|
+
rows = need_biases(needs, baseline)
|
|
115
138
|
|
|
116
139
|
def deficit(key):
|
|
117
|
-
return
|
|
140
|
+
return _clamp(rows.get(key, {}).get("deficit"), 0.0, 1.0)
|
|
118
141
|
|
|
119
142
|
def surplus(key):
|
|
120
|
-
return
|
|
143
|
+
return _clamp(rows.get(key, {}).get("surplus"), 0.0, 1.0)
|
|
121
144
|
|
|
122
145
|
return {
|
|
123
146
|
"seek_contact": round(_clamp(3.0 * deficit("belonging"), 0.0, 1.0), 3),
|
|
124
|
-
"self_assert": round(_clamp(2.0 * surplus("self_esteem")
|
|
125
|
-
|
|
126
|
-
"ask_clarify": round(_clamp(2.0 * deficit("control")
|
|
127
|
-
+ 1.0 * deficit("meaningful_existence"),
|
|
128
|
-
0.0, 1.0), 3),
|
|
147
|
+
"self_assert": round(_clamp(2.0 * surplus("self_esteem"), 0.0, 1.0), 3),
|
|
148
|
+
"ask_clarify": round(_clamp(2.0 * deficit("control"), 0.0, 1.0), 3),
|
|
129
149
|
"share_meaning": round(_clamp(2.5 * surplus("meaningful_existence"),
|
|
130
150
|
0.0, 1.0), 3),
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
|
|
134
154
|
def summary(needs):
|
|
135
|
-
"""Bounded row for logs/panel."""
|
|
155
|
+
"""Bounded row for logs/panel: the four actions plus every need line."""
|
|
136
156
|
state = needs_state(needs)
|
|
137
|
-
return {"needs": state, "behavior": behavior_bias(state)
|
|
157
|
+
return {"needs": state, "behavior": behavior_bias(state),
|
|
158
|
+
"bias": need_biases(state)}
|
|
@@ -228,6 +228,11 @@ def profile(strategy, state=None, event=None, relationship=None):
|
|
|
228
228
|
"cost": round(cost, 3),
|
|
229
229
|
"effect": round(effect, 3),
|
|
230
230
|
"score": round(score, 3),
|
|
231
|
+
# never a ratio: effect and cost keep their own numbers, the score is
|
|
232
|
+
# only the display order the caller sorts by
|
|
233
|
+
"score_parts": {"fit": round(fit, 3), "effect": round(effect, 3),
|
|
234
|
+
"cost": round(cost, 3)},
|
|
235
|
+
"score_is_display": True,
|
|
231
236
|
"coping_tier": TIER_OF[strategy],
|
|
232
237
|
"reason": REASONS[strategy],
|
|
233
238
|
"self_talk": SELF_TALK[strategy],
|
|
@@ -30,6 +30,7 @@ STAGES = ("cold", "state", "record", "turn_page")
|
|
|
30
30
|
LABELS = {"cold": "冷处理", "state": "明说", "record": "记账",
|
|
31
31
|
"turn_page": "翻页"}
|
|
32
32
|
|
|
33
|
+
# [engineering] stage durations and the forgive floor are hand-tuned.
|
|
33
34
|
COLD_SECONDS = 300.0 # cold -> state
|
|
34
35
|
STATE_SECONDS = 1800.0 # state -> record
|
|
35
36
|
RECORD_SECONDS = 7200.0 # record -> turn_page
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
BASELINE = 0.55
|
|
9
9
|
REGRESS = 0.05
|
|
10
10
|
EVENT_DELTAS = {"praised": 0.06, "replied": 0.06, "ignored": -0.05, "insulted": -0.05}
|
|
11
|
+
EVENT_KEEP = 24
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def clamp(value):
|
|
@@ -22,9 +23,16 @@ def initial(now=0.0):
|
|
|
22
23
|
def update(state, event_type, now):
|
|
23
24
|
data = dict(state or initial(now))
|
|
24
25
|
delta = EVENT_DELTAS.get(str(event_type or ""), 0.0)
|
|
25
|
-
|
|
26
|
+
before = clamp(float(data.get("value", BASELINE)))
|
|
27
|
+
data["value"] = round(clamp(before + delta), 4)
|
|
26
28
|
data["last_event"] = str(event_type or "")
|
|
27
29
|
data["last_event_ts"] = float(now)
|
|
30
|
+
# every event keeps its own row; "value" is only the running aggregate
|
|
31
|
+
events = [row for row in (data.get("events") or []) if isinstance(row, dict)]
|
|
32
|
+
events.append({"event": str(event_type or ""), "delta": round(delta, 4),
|
|
33
|
+
"before": round(before, 4), "after": data["value"],
|
|
34
|
+
"ts": float(now)})
|
|
35
|
+
data["events"] = events[-EVENT_KEEP:]
|
|
28
36
|
return data
|
|
29
37
|
|
|
30
38
|
|
|
@@ -46,5 +54,9 @@ def confidence_delta(value):
|
|
|
46
54
|
def view(state):
|
|
47
55
|
data = dict(state or initial())
|
|
48
56
|
value = round(clamp(float(data.get("value", BASELINE))), 3)
|
|
49
|
-
return {"value": value, "baseline": BASELINE,
|
|
57
|
+
return {"value": value, "baseline": BASELINE,
|
|
58
|
+
"last_event": str(data.get("last_event") or ""),
|
|
59
|
+
"events": [dict(row) for row in (data.get("events") or [])
|
|
60
|
+
if isinstance(row, dict)],
|
|
61
|
+
"value_is_aggregate": True,
|
|
50
62
|
"confidence_delta": confidence_delta(value)}
|
|
@@ -26,8 +26,10 @@ from __future__ import annotations
|
|
|
26
26
|
import math
|
|
27
27
|
|
|
28
28
|
NEED_KEYS = ("belonging", "self_esteem", "control", "meaningful_existence")
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
# Deci & Ryan 2000, Psychological Inquiry 11(4):227-268: the basic needs have
|
|
30
|
+
# equal standing, so every slot weighs the same instead of .30/.25/.20/.25.
|
|
31
|
+
NEED_WEIGHTS = {"belonging": 0.25, "self_esteem": 0.25,
|
|
32
|
+
"control": 0.25, "meaningful_existence": 0.25}
|
|
31
33
|
KINDS = ("要你陪", "自己待着", "拒绝话题", "顺着你", "说真话")
|
|
32
34
|
|
|
33
35
|
# Expectations start near the "just fine" line and drift with experience.
|
|
@@ -40,8 +42,8 @@ DEFAULT_EXPECTED = {
|
|
|
40
42
|
EXPECTED_RATE = 0.03 # per-call drift, never a jump
|
|
41
43
|
SELF_WEIGHT_FLOOR = 0.10
|
|
42
44
|
SELF_WEIGHT_CEIL = 0.90
|
|
43
|
-
MIN_WEIGHT = 0.15 # intents below this are dropped
|
|
44
|
-
MAX_INTENTS = 2
|
|
45
|
+
MIN_WEIGHT = 0.15 # [engineering] intents below this are dropped
|
|
46
|
+
MAX_INTENTS = 2 # [engineering]
|
|
45
47
|
MAX_CANDIDATES = 4
|
|
46
48
|
SELF_HANDLE_URGE = 0.35 # below this she tends to handle it herself
|
|
47
49
|
OPEN_UP_URGE = 0.50 # above this she tends to ask / speak up
|