dsh-biocore 0.4.0 → 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/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_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 -137
- 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 -153
- package/python/biocore_model/tests/test_memory_inject.py +211 -211
- package/python/biocore_model/tests/test_retrieval_plus.py +143 -143
- package/python/biocore_model/tests/test_runtime_pipeline.py +1 -1
- package/python/biocore_model/tests/test_signals.py +106 -106
- package/python/biocore_model/tests/test_standby.py +199 -199
- 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/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/perception.py +1045 -0
- package/python/biocore_runtime/pipeline.py +4458 -3105
- package/python/biocore_runtime/priority.py +210 -0
- package/python/biocore_runtime/standby.py +581 -579
- package/python/biocore_runtime/timescales.py +263 -0
- 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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-244 step 1: shadow coordinates for memory entries (write-only).
|
|
3
|
+
|
|
4
|
+
Every entry gets a read-only "space" dict with five axes:
|
|
5
|
+
|
|
6
|
+
semantic the embedding vector (reused when the entry already has one)
|
|
7
|
+
with its provider / model name, plus topic / entity tags and a
|
|
8
|
+
text fingerprint fallback. semantic distance = 1 - cosine
|
|
9
|
+
(Jaccard over tags when either side has no vector)
|
|
10
|
+
time the entry ts, its own half-life in minutes, its age
|
|
11
|
+
affect v / a as they stood when the entry was written
|
|
12
|
+
social scope + trust / closeness at write time
|
|
13
|
+
activation importance / hits / retrieval_count / last_hit
|
|
14
|
+
|
|
15
|
+
Boundaries
|
|
16
|
+
Step 1 only adds coordinates. recall(), ranking and the memory score never
|
|
17
|
+
read this module, and recall keeps its old behaviour byte for byte.
|
|
18
|
+
The switch spatial_recall is OFF by default; with it off nothing is written.
|
|
19
|
+
A coordinate that cannot be built returns None and the entry is untouched.
|
|
20
|
+
|
|
21
|
+
Distances are reported PER AXIS. They are never added into one number; the
|
|
22
|
+
nearest list is ordered lexicographically (semantic, affect, time,
|
|
23
|
+
activation, social), which keeps every axis visible in the ranking.
|
|
24
|
+
|
|
25
|
+
Weights / half-lives below are [engineering] (see docs/PAPER-MAP.md).
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import math
|
|
31
|
+
import re
|
|
32
|
+
|
|
33
|
+
SCHEMA = 1
|
|
34
|
+
SWITCH_KEY = "spatial_recall"
|
|
35
|
+
ENV_KEY = "BIOCORE_SPATIAL_RECALL"
|
|
36
|
+
AXES = ("semantic", "time", "affect", "social", "activation")
|
|
37
|
+
DISTANCE_ORDER = ("semantic", "affect", "time", "activation", "social")
|
|
38
|
+
|
|
39
|
+
VECTOR_LIMIT = 768
|
|
40
|
+
TOPIC_LIMIT = 8
|
|
41
|
+
ENTITY_LIMIT = 8
|
|
42
|
+
FINGERPRINT_CHARS = 12
|
|
43
|
+
|
|
44
|
+
BASE_HALF_LIFE_MINUTES = 180.0 # [engineering]
|
|
45
|
+
HALF_LIFE_BY_KIND = {"direct": 180.0, "recall": 120.0,
|
|
46
|
+
"summary": 240.0, "reflection": 360.0} # [engineering]
|
|
47
|
+
IMPORTANCE_SPAN = 0.5 # half-life scales 0.5x .. 1.5x
|
|
48
|
+
HITS_CEILING = 20.0 # [engineering] log scale for the hits axis
|
|
49
|
+
DEFAULT_IMPORTANCE = 0.5
|
|
50
|
+
DEFAULT_TRUST = 0.5
|
|
51
|
+
DEFAULT_CLOSENESS = 0.5
|
|
52
|
+
|
|
53
|
+
_TOKEN = re.compile(r"[a-z0-9_]+") # latin words
|
|
54
|
+
_CJK_RUN = re.compile(r"[\u4e00-\u9fff]+") # CJK runs, split into bigrams below
|
|
55
|
+
_WS = re.compile(r"\s+")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _num(value, default=0.0):
|
|
59
|
+
try:
|
|
60
|
+
number = float(value)
|
|
61
|
+
except Exception:
|
|
62
|
+
return float(default)
|
|
63
|
+
if not math.isfinite(number):
|
|
64
|
+
return float(default)
|
|
65
|
+
return number
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _clamp(value, low=0.0, high=1.0):
|
|
69
|
+
return max(low, min(high, _num(value, low)))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def enabled(flags=None, env=None):
|
|
73
|
+
"""spatial_recall: explicit flag > nested switches > env > default OFF."""
|
|
74
|
+
data = flags if isinstance(flags, dict) else {}
|
|
75
|
+
if isinstance(data.get(SWITCH_KEY), bool):
|
|
76
|
+
return data[SWITCH_KEY]
|
|
77
|
+
switches = data.get("switches") if isinstance(data.get("switches"), dict) else {}
|
|
78
|
+
if isinstance(switches.get(SWITCH_KEY), bool):
|
|
79
|
+
return switches[SWITCH_KEY]
|
|
80
|
+
environ = env if hasattr(env, "get") else None
|
|
81
|
+
if environ is not None and ENV_KEY in environ:
|
|
82
|
+
raw = str(environ.get(ENV_KEY)).strip().lower()
|
|
83
|
+
if raw in ("1", "true", "on", "yes"):
|
|
84
|
+
return True
|
|
85
|
+
if raw in ("0", "false", "off", "no"):
|
|
86
|
+
return False
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def tokens(text):
|
|
91
|
+
"""Deterministic bag: latin words + CJK bigrams + single characters.
|
|
92
|
+
|
|
93
|
+
Bigrams are what the governance concept extractor produces, so a query and
|
|
94
|
+
an entry's topics line up when no embedding is available.
|
|
95
|
+
"""
|
|
96
|
+
body = _WS.sub(" ", str(text or "")).lower()
|
|
97
|
+
out = _TOKEN.findall(body)
|
|
98
|
+
for run in _CJK_RUN.findall(body):
|
|
99
|
+
if len(run) == 1:
|
|
100
|
+
out.append(run)
|
|
101
|
+
continue
|
|
102
|
+
out.extend(run[index:index + 2] for index in range(len(run) - 1))
|
|
103
|
+
out.extend(run)
|
|
104
|
+
seen = []
|
|
105
|
+
for item in out:
|
|
106
|
+
if item not in seen:
|
|
107
|
+
seen.append(item)
|
|
108
|
+
return seen
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def fingerprint(text):
|
|
112
|
+
body = _WS.sub(" ", str(text or "")).strip().lower()
|
|
113
|
+
return hashlib.sha1(body.encode("utf-8")).hexdigest()[:FINGERPRINT_CHARS]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def half_life_minutes(entry):
|
|
117
|
+
"""Per-entry half-life: the kind's base, scaled by importance."""
|
|
118
|
+
entry = entry if isinstance(entry, dict) else {}
|
|
119
|
+
kind = str(entry.get("kind") or entry.get("memory_kind") or "direct")
|
|
120
|
+
base = HALF_LIFE_BY_KIND.get(kind, BASE_HALF_LIFE_MINUTES)
|
|
121
|
+
importance = _clamp(entry.get("importance", DEFAULT_IMPORTANCE))
|
|
122
|
+
return round(base * (1.0 - IMPORTANCE_SPAN + 2.0 * IMPORTANCE_SPAN * importance),
|
|
123
|
+
3)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _vector(entry):
|
|
127
|
+
raw = entry.get("vector") if isinstance(entry, dict) else None
|
|
128
|
+
if not isinstance(raw, (list, tuple)):
|
|
129
|
+
return []
|
|
130
|
+
out = []
|
|
131
|
+
for value in raw[:VECTOR_LIMIT]:
|
|
132
|
+
try:
|
|
133
|
+
number = float(value)
|
|
134
|
+
except Exception:
|
|
135
|
+
continue
|
|
136
|
+
if math.isfinite(number):
|
|
137
|
+
out.append(round(number, 6))
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _tags(values, limit):
|
|
142
|
+
if isinstance(values, str):
|
|
143
|
+
values = [values]
|
|
144
|
+
if not isinstance(values, (list, tuple)):
|
|
145
|
+
return []
|
|
146
|
+
out = []
|
|
147
|
+
for value in values:
|
|
148
|
+
text = str(value or "").strip()
|
|
149
|
+
if text and text not in out:
|
|
150
|
+
out.append(text[:32])
|
|
151
|
+
return out[:limit] # tags first, then concepts: order is meaningful
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _embed_names(entry, embed=None):
|
|
155
|
+
"""Provider / model of the embedding, so mismatches are visible at a glance."""
|
|
156
|
+
entry = entry if isinstance(entry, dict) else {}
|
|
157
|
+
info = embed if isinstance(embed, dict) else {}
|
|
158
|
+
provider = str(info.get("provider")
|
|
159
|
+
or entry.get("embedding_provider")
|
|
160
|
+
or entry.get("vector_provider")
|
|
161
|
+
or entry.get("embed_provider") or "")[:64]
|
|
162
|
+
model = str(info.get("model")
|
|
163
|
+
or entry.get("embedding_model")
|
|
164
|
+
or entry.get("vector_model")
|
|
165
|
+
or entry.get("embed_model") or "")[:64]
|
|
166
|
+
return provider, model
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def build(entry, relationship=None, now=None, embed=None):
|
|
170
|
+
"""Five-axis coordinates for one entry; None when they cannot be built."""
|
|
171
|
+
try:
|
|
172
|
+
return _build(entry, relationship, now, embed)
|
|
173
|
+
except Exception: # a broken entry never breaks the write
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _build(entry, relationship=None, now=None, embed=None):
|
|
178
|
+
if not isinstance(entry, dict):
|
|
179
|
+
return None # nothing to put coordinates on
|
|
180
|
+
rel = relationship if isinstance(relationship, dict) else {}
|
|
181
|
+
embed_provider, embed_model = _embed_names(entry, embed)
|
|
182
|
+
stamp = _num(now, _num(entry.get("ts"), 0.0))
|
|
183
|
+
ts = round(_num(entry.get("ts"), stamp), 3)
|
|
184
|
+
vector = _vector(entry)
|
|
185
|
+
topics = _tags(list(entry.get("tags") or []) + list(entry.get("concepts") or []),
|
|
186
|
+
TOPIC_LIMIT)
|
|
187
|
+
entities = _tags(entry.get("entities") or entry.get("entity"), ENTITY_LIMIT)
|
|
188
|
+
affect = entry.get("affect") if isinstance(entry.get("affect"), dict) else {}
|
|
189
|
+
trust = rel.get("trust", entry.get("trust", DEFAULT_TRUST))
|
|
190
|
+
closeness = rel.get("closeness", entry.get("closeness", DEFAULT_CLOSENESS))
|
|
191
|
+
return {
|
|
192
|
+
"schema": SCHEMA,
|
|
193
|
+
"semantic": {
|
|
194
|
+
"vector": vector,
|
|
195
|
+
"vector_dim": len(vector),
|
|
196
|
+
# true = tags only; never pretend a vector exists
|
|
197
|
+
"fallback": not bool(vector),
|
|
198
|
+
"provider": embed_provider,
|
|
199
|
+
"model": embed_model,
|
|
200
|
+
"topics": topics,
|
|
201
|
+
"entities": entities,
|
|
202
|
+
"fingerprint": fingerprint(entry.get("text")),
|
|
203
|
+
},
|
|
204
|
+
"time": {
|
|
205
|
+
"ts": ts,
|
|
206
|
+
"half_life_minutes": half_life_minutes(entry),
|
|
207
|
+
"age_minutes": round(max(0.0, stamp - ts) / 60.0, 3),
|
|
208
|
+
"last_hit_ts": round(_num(entry.get("last_hit_ts"), 0.0), 3),
|
|
209
|
+
},
|
|
210
|
+
"affect": {
|
|
211
|
+
"v": round(_clamp(affect.get("v", entry.get("v", 0.0)), -1.0, 1.0), 6),
|
|
212
|
+
"a": round(_clamp(affect.get("a", entry.get("a", 0.5))), 6),
|
|
213
|
+
},
|
|
214
|
+
"social": {
|
|
215
|
+
"scope": str(entry.get("scope") or entry.get("session_id") or "default")[:64],
|
|
216
|
+
"trust": round(_clamp(trust), 6),
|
|
217
|
+
"closeness": round(_clamp(closeness), 6),
|
|
218
|
+
},
|
|
219
|
+
"activation": {
|
|
220
|
+
"importance": round(_clamp(entry.get("importance", DEFAULT_IMPORTANCE)), 6),
|
|
221
|
+
"hits": int(_num(entry.get("hits"), 1)),
|
|
222
|
+
"retrieval_count": int(_num(entry.get("retrieval_count"), 0)),
|
|
223
|
+
"last_hit_ts": round(_num(entry.get("last_hit_ts"), 0.0), 3),
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------ distance
|
|
229
|
+
def _cosine_distance(x, y):
|
|
230
|
+
dot = sum(a * b for a, b in zip(x, y))
|
|
231
|
+
norm_x = math.sqrt(sum(a * a for a in x))
|
|
232
|
+
norm_y = math.sqrt(sum(b * b for b in y))
|
|
233
|
+
if norm_x <= 0 or norm_y <= 0:
|
|
234
|
+
return 1.0
|
|
235
|
+
return _clamp(1.0 - dot / (norm_x * norm_y), 0.0, 2.0)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _semantic_distance(x, y):
|
|
239
|
+
x = x if isinstance(x, dict) else {}
|
|
240
|
+
y = y if isinstance(y, dict) else {}
|
|
241
|
+
vx = _vector({"vector": x.get("vector")})
|
|
242
|
+
vy = _vector({"vector": y.get("vector")})
|
|
243
|
+
if vx and vy and len(vx) == len(vy):
|
|
244
|
+
# 1 - cosine: 0 for the same direction, 2 for the opposite one
|
|
245
|
+
return round(_clamp(_cosine_distance(vx, vy), 0.0, 2.0), 6)
|
|
246
|
+
tx, ty = set(x.get("topics") or []), set(y.get("topics") or [])
|
|
247
|
+
ex, ey = set(x.get("entities") or []), set(y.get("entities") or [])
|
|
248
|
+
union = tx | ty | ex | ey
|
|
249
|
+
if not union:
|
|
250
|
+
return 1.0
|
|
251
|
+
shared = (tx & ty) | (ex & ey)
|
|
252
|
+
return round(1.0 - len(shared) / len(union), 6)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _time_distance(x, y):
|
|
256
|
+
x = x if isinstance(x, dict) else {}
|
|
257
|
+
y = y if isinstance(y, dict) else {}
|
|
258
|
+
gap = abs(_num(x.get("ts"), 0.0) - _num(y.get("ts"), 0.0))
|
|
259
|
+
scale = max(1.0, _num(x.get("half_life_minutes"), BASE_HALF_LIFE_MINUTES),
|
|
260
|
+
_num(y.get("half_life_minutes"), BASE_HALF_LIFE_MINUTES)) * 60.0
|
|
261
|
+
return round(_clamp(gap / scale), 6)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _affect_distance(x, y):
|
|
265
|
+
x = x if isinstance(x, dict) else {}
|
|
266
|
+
y = y if isinstance(y, dict) else {}
|
|
267
|
+
dv = _num(x.get("v"), 0.0) - _num(y.get("v"), 0.0)
|
|
268
|
+
da = _num(x.get("a"), 0.5) - _num(y.get("a"), 0.5)
|
|
269
|
+
return round(_clamp(math.sqrt((dv * dv + da * da) / 2.0)), 6)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _social_distance(x, y):
|
|
273
|
+
x = x if isinstance(x, dict) else {}
|
|
274
|
+
y = y if isinstance(y, dict) else {}
|
|
275
|
+
scope = 0.0 if str(x.get("scope") or "") == str(y.get("scope") or "") else 1.0
|
|
276
|
+
spread = (abs(_num(x.get("trust"), DEFAULT_TRUST)
|
|
277
|
+
- _num(y.get("trust"), DEFAULT_TRUST))
|
|
278
|
+
+ abs(_num(x.get("closeness"), DEFAULT_CLOSENESS)
|
|
279
|
+
- _num(y.get("closeness"), DEFAULT_CLOSENESS))) / 2.0
|
|
280
|
+
return round(_clamp(0.5 * scope + 0.5 * spread), 6)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _activation_distance(x, y):
|
|
284
|
+
x = x if isinstance(x, dict) else {}
|
|
285
|
+
y = y if isinstance(y, dict) else {}
|
|
286
|
+
importance = abs(_num(x.get("importance"), DEFAULT_IMPORTANCE)
|
|
287
|
+
- _num(y.get("importance"), DEFAULT_IMPORTANCE))
|
|
288
|
+
ceiling = math.log1p(HITS_CEILING)
|
|
289
|
+
hits = abs(math.log1p(max(0.0, _num(x.get("hits"), 0.0)))
|
|
290
|
+
- math.log1p(max(0.0, _num(y.get("hits"), 0.0)))) / ceiling
|
|
291
|
+
return round(_clamp(0.6 * importance + 0.4 * hits), 6)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def distance(space_a, space_b):
|
|
295
|
+
"""Per-axis distance; there is no total, on purpose."""
|
|
296
|
+
a = space_a if isinstance(space_a, dict) else {}
|
|
297
|
+
b = space_b if isinstance(space_b, dict) else {}
|
|
298
|
+
return {
|
|
299
|
+
"semantic": _semantic_distance(a.get("semantic"), b.get("semantic")),
|
|
300
|
+
"time": _time_distance(a.get("time"), b.get("time")),
|
|
301
|
+
"affect": _affect_distance(a.get("affect"), b.get("affect")),
|
|
302
|
+
"social": _social_distance(a.get("social"), b.get("social")),
|
|
303
|
+
"activation": _activation_distance(a.get("activation"),
|
|
304
|
+
b.get("activation")),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def order_key(distances):
|
|
309
|
+
"""Lexicographic order over the axes; nothing is summed."""
|
|
310
|
+
row = distances if isinstance(distances, dict) else {}
|
|
311
|
+
return tuple(_num(row.get(axis), 1.0) for axis in DISTANCE_ORDER)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# --------------------------------------------------------------------- probe
|
|
315
|
+
def query_space(query, vector=None, now=None, embed=None):
|
|
316
|
+
"""Coordinates for a bare query string (same five axes, no id)."""
|
|
317
|
+
return build({"text": query, "vector": vector or [],
|
|
318
|
+
"ts": _num(now, 0.0), "tags": tokens(query),
|
|
319
|
+
"importance": DEFAULT_IMPORTANCE}, now=now, embed=embed)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def probe(entries, entry_id=None, query=None, vector=None, limit=5, now=None):
|
|
323
|
+
"""Debug read: one entry's five axes, or the nearest entries to a query."""
|
|
324
|
+
rows = [row for row in (entries or []) if isinstance(row, dict)]
|
|
325
|
+
limit = max(1, min(20, int(_num(limit, 5))))
|
|
326
|
+
if entry_id:
|
|
327
|
+
want = str(entry_id)
|
|
328
|
+
for row in rows:
|
|
329
|
+
if str(row.get("id") or "") == want:
|
|
330
|
+
stored = row.get("space")
|
|
331
|
+
space = stored if isinstance(stored, dict) else build(row, now=now)
|
|
332
|
+
return {"ok": space is not None, "id": want,
|
|
333
|
+
"source": "entry" if isinstance(stored, dict) else "computed",
|
|
334
|
+
"space": space,
|
|
335
|
+
"reason": "" if space is not None else "build_failed"}
|
|
336
|
+
return {"ok": False, "reason": "not_found", "id": want}
|
|
337
|
+
if query:
|
|
338
|
+
target = query_space(query, vector=vector, now=now)
|
|
339
|
+
scored = []
|
|
340
|
+
for row in rows:
|
|
341
|
+
stored = row.get("space")
|
|
342
|
+
space = stored if isinstance(stored, dict) else build(row, now=now)
|
|
343
|
+
if not isinstance(space, dict):
|
|
344
|
+
continue
|
|
345
|
+
distances = distance(space, target)
|
|
346
|
+
scored.append({"id": str(row.get("id") or ""),
|
|
347
|
+
"text": str(row.get("text") or "")[:80],
|
|
348
|
+
"ts": round(_num(row.get("ts"), 0.0), 3),
|
|
349
|
+
"distance": distances,
|
|
350
|
+
"from": "entry" if isinstance(stored, dict) else "computed"})
|
|
351
|
+
scored.sort(key=lambda item: order_key(item["distance"]))
|
|
352
|
+
return {"ok": True, "query": str(query), "count": len(scored),
|
|
353
|
+
"limit": limit, "order": list(DISTANCE_ORDER),
|
|
354
|
+
"space": target, "results": scored[:limit]}
|
|
355
|
+
return {"ok": False, "reason": "id_or_query_required"}
|
|
@@ -26,7 +26,7 @@ DIM_WEIGHTS = {
|
|
|
26
26
|
"trust": 0.10,
|
|
27
27
|
"stress": 0.10,
|
|
28
28
|
}
|
|
29
|
-
MASK_COST = 0.22
|
|
29
|
+
MASK_COST = 0.22 # [engineering] energy cost of surface acting, hand-tuned
|
|
30
30
|
LOAD_DECAY_PER_HOUR = 0.03
|
|
31
31
|
BREAKDOWN_THRESHOLD = 0.85
|
|
32
32
|
|
|
@@ -121,6 +121,12 @@ def update(labor_state, expressed, felt, honesty=0.0, now=0.0,
|
|
|
121
121
|
load = _clamp01(load + cost)
|
|
122
122
|
data["load"] = round(load, 4)
|
|
123
123
|
data["last_cost"] = cost
|
|
124
|
+
# Three components stay separate: a single "cost" hid which one moved.
|
|
125
|
+
data["components"] = {
|
|
126
|
+
"emotional_exhaustion": round(load, 4),
|
|
127
|
+
"depersonalization": round(mask, 4),
|
|
128
|
+
"personal_accomplishment": round(max(0.0, 1.0 - mask), 4),
|
|
129
|
+
}
|
|
124
130
|
data["last_masking"] = mask
|
|
125
131
|
data["turns"] = int(data.get("turns", 0) or 0) + 1
|
|
126
132
|
if load >= BREAKDOWN_THRESHOLD and before < BREAKDOWN_THRESHOLD:
|
|
@@ -157,6 +163,13 @@ def view(labor_state):
|
|
|
157
163
|
"collapsed": collapsed(data),
|
|
158
164
|
"last_cost": round(_clamp01(data.get("last_cost", 0.0)), 4),
|
|
159
165
|
"last_masking": round(_clamp01(data.get("last_masking", 0.0)), 4),
|
|
166
|
+
"components": {
|
|
167
|
+
"emotional_exhaustion": round(_clamp01(data.get("load", 0.0)), 4),
|
|
168
|
+
"depersonalization": round(
|
|
169
|
+
_clamp01(data.get("last_masking", 0.0)), 4),
|
|
170
|
+
"personal_accomplishment": round(
|
|
171
|
+
max(0.0, 1.0 - _clamp01(data.get("last_masking", 0.0))), 4),
|
|
172
|
+
},
|
|
160
173
|
"turns": int(data.get("turns", 0) or 0),
|
|
161
174
|
"breakdowns": int(data.get("breakdowns", 0) or 0),
|
|
162
175
|
}
|
|
@@ -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)
|