@a9i5k4/dsh-auto-memory 2.1.5 → 2.1.6
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/package.json +6 -3
- package/python/m7_activation_features_v2.py +392 -0
- package/python/m7_embedding_v1.py +381 -0
- package/python/policies/activation_policy_v2.json +88 -0
- package/python/policies/decision-record-activation-v2-delta-exp-override-20260824.json +21 -0
- package/python/policies/decision-record-reasoning-kind-admission-20260826.json +20 -0
- package/python/policies/decision-record-stale-gate-per-candidate-20260825.json +33 -0
- package/python/policies/recall_intent_lr_v1.json +1 -0
- package/python/verify_policy_artifact.py +122 -0
- package/python/worker_semantic_v1.py +1344 -0
- package/python/worker_v1.py +623 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a9i5k4/dsh-auto-memory",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "2.1.
|
|
3
|
+
"description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
|
|
4
|
+
"version": "2.1.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"lib",
|
|
14
|
-
"
|
|
14
|
+
"python",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"!python/bench",
|
|
17
|
+
"!python/__pycache__"
|
|
15
18
|
],
|
|
16
19
|
"dsh": {
|
|
17
20
|
"bundle": {
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""M7 activation features pre v2 (policy: activation_policy_v2).
|
|
4
|
+
|
|
5
|
+
Round-1 implementation of the two-lane activation eligibility decision.
|
|
6
|
+
Pure stdlib; deterministic; NO pickle/joblib/sklearn at runtime — the
|
|
7
|
+
calibrated intent head is reproduced from the auditable JSON artifact
|
|
8
|
+
python/policies/recall_intent_lr_v1.json (vocabulary + IDF + logistic
|
|
9
|
+
regression coefficients/intercept + Platt calibration), exported by the
|
|
10
|
+
calibration pipeline (runId label-review-cal20260824-1954).
|
|
11
|
+
|
|
12
|
+
Decision order (main-Agent amendment 2026-08-25, supersedes any earlier
|
|
13
|
+
draft that placed echo veto globally):
|
|
14
|
+
|
|
15
|
+
1. JS/M7 hard gates : harmful / correction / ignored / stale /
|
|
16
|
+
wrong_scope / pii_high -> suppress
|
|
17
|
+
2. lane decision : intentProb >= tauLane -> explicit, else proactive
|
|
18
|
+
3. explicit lane : echoRisk is a FEATURE here, never a veto
|
|
19
|
+
emit : intent>=tauHi AND candidateHit AND
|
|
20
|
+
margin>=deltaExp AND completeness==complete
|
|
21
|
+
prefetch : intent>=tauHi AND hit AND
|
|
22
|
+
completeness in (partial, unknown)
|
|
23
|
+
OR intent>=tauLo AND hit
|
|
24
|
+
4. proactive lane : high echoRisk (containmentArm|denseTopArm) with
|
|
25
|
+
statement form and intent<cap -> hard suppress;
|
|
26
|
+
margin>=deltaPro AND nCand>=2 AND low intent AND
|
|
27
|
+
denseTop<arm -> prefetch
|
|
28
|
+
5. default suppress
|
|
29
|
+
|
|
30
|
+
repetition this round is logging-only: counts may upgrade suppress->prefetch,
|
|
31
|
+
never activate on counts alone; life/chitchat topics never escalate.
|
|
32
|
+
|
|
33
|
+
Fail-closed policy: load_and_verify_policy refuses to serve when the JSON is
|
|
34
|
+
missing fields, length-inconsistent, or configHash does not recompute. The
|
|
35
|
+
hash algorithm mirrors export_v2_artifacts.py exactly:
|
|
36
|
+
payload = json.dumps(policy_minus_configHash, sort_keys=True,
|
|
37
|
+
ensure_ascii=False)
|
|
38
|
+
configHash = 'cfgh_' + sha256(payload.encode('utf-8')).hexdigest()[:32]
|
|
39
|
+
|
|
40
|
+
Public API (pure functions / pure objects):
|
|
41
|
+
normalize_text, infer_recall_intent, infer_dialogue_act, infer_task_need,
|
|
42
|
+
compute_echo_risk, compute_completeness, compute_lane,
|
|
43
|
+
decide_activation_v2, load_and_verify_policy, replay_features_v2
|
|
44
|
+
"""
|
|
45
|
+
import hashlib
|
|
46
|
+
import json
|
|
47
|
+
import math
|
|
48
|
+
import os
|
|
49
|
+
import re
|
|
50
|
+
|
|
51
|
+
FEATURES_POLICY_VERSION = 'activation_features_v2'
|
|
52
|
+
INTENT_POLICY_VERSION = 'recall_intent_lr_v1'
|
|
53
|
+
ACTIVATION_POLICY_VERSION = 'activation_policy_v2'
|
|
54
|
+
|
|
55
|
+
INTERROG = ['什么', '如何', '怎么', '哪些', '哪个', '为什么', '多少', '吗',
|
|
56
|
+
'呢', '啥', 'recall', 'what', 'how', 'which', 'when', 'where',
|
|
57
|
+
'why', 'who']
|
|
58
|
+
RECALL_CTX = ['之前', '上次', '当时', '早前', '以往', '历史', '记录里', '记忆里',
|
|
59
|
+
'找出来', '调出来', '翻下', '说下', 'previous', 'earlier',
|
|
60
|
+
'last time']
|
|
61
|
+
COMPLETENESS_LEXICON_DEFAULT = ['对比', '分别', '两个', '一起', '都调', '各自']
|
|
62
|
+
ACK_TOKENS = ['好的', '嗯嗯', '谢谢', '晚安', '收到']
|
|
63
|
+
ERR_TOKENS = ['又失败', '又超限', '又不对', '第三次', '报错', '又出现', '又丢']
|
|
64
|
+
REQ_TOKENS = ['帮我', '找出来', '调出来', '说一下', '再讲讲', '发我']
|
|
65
|
+
PLAN_TOKENS = ['准备', '打算', '计划', '之后', '接下来', '继续']
|
|
66
|
+
|
|
67
|
+
_WORD_RE = re.compile(r'(?u)\b\w\w+\b')
|
|
68
|
+
_WS_RUN = re.compile(r'\s\s+')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------- text utils
|
|
72
|
+
|
|
73
|
+
def normalize_text(text):
|
|
74
|
+
"""Lowercase; keep [a-z0-9] and CJK; drop everything else.
|
|
75
|
+
|
|
76
|
+
Must byte-match the calibration exporter's normalize_text so that
|
|
77
|
+
vocabulary lookups are stable across offline/online."""
|
|
78
|
+
return ''.join(ch for ch in str(text).lower()
|
|
79
|
+
if ch.isalnum() or '\u4e00' <= ch <= '\u9fff')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _char_wb_ngram_counts(norm_text, min_n, max_n):
|
|
83
|
+
"""sklearn char_wb parity: collapse whitespace runs, tokenize with
|
|
84
|
+
(?u)\\b\\w\\w+\\b, pad each word with spaces, take char n-grams."""
|
|
85
|
+
t = _WS_RUN.sub(' ', norm_text)
|
|
86
|
+
counts = {}
|
|
87
|
+
for word in _WORD_RE.findall(t):
|
|
88
|
+
padded = ' ' + word + ' '
|
|
89
|
+
L = len(padded)
|
|
90
|
+
for n in range(min_n, min(max_n, L) + 1):
|
|
91
|
+
for i in range(L - n + 1):
|
|
92
|
+
gram = padded[i:i + n]
|
|
93
|
+
counts[gram] = counts.get(gram, 0) + 1
|
|
94
|
+
return counts
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def bigram_set(text):
|
|
98
|
+
t = normalize_text(text)
|
|
99
|
+
return set(t[i:i + 2] for i in range(len(t) - 1)) or {t}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def lexical_containment(query_text, candidate_text):
|
|
103
|
+
q = bigram_set(query_text)
|
|
104
|
+
c = bigram_set(candidate_text)
|
|
105
|
+
if not q:
|
|
106
|
+
return 0.0
|
|
107
|
+
return len(q & c) / len(q)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------ intent head
|
|
111
|
+
|
|
112
|
+
class RecallIntentHead:
|
|
113
|
+
"""Deterministic pure-Python reproduction of the calibrated LR head."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, artifact):
|
|
116
|
+
fs = artifact['featureSchema']
|
|
117
|
+
vf = fs['vectorizer']
|
|
118
|
+
assert vf['analyzer'] == 'char_wb' and vf['minDf'] == 1 \
|
|
119
|
+
and vf['sublinearTf'] is True and vf['ngramRange'] == [2, 4], \
|
|
120
|
+
'unsupported featureSchema'
|
|
121
|
+
self.min_n, self.max_n = vf['ngramRange']
|
|
122
|
+
self.vocab = artifact['vocabulary']
|
|
123
|
+
self.idf = artifact['idf']
|
|
124
|
+
self.coef = artifact['coefficients']
|
|
125
|
+
self.intercept = float(artifact['intercept'])
|
|
126
|
+
cal = artifact['calibration']
|
|
127
|
+
assert cal['method'] == 'platt'
|
|
128
|
+
self.platt_a = float(cal['a'])
|
|
129
|
+
self.platt_b = float(cal['b'])
|
|
130
|
+
self._norm_cache = {}
|
|
131
|
+
|
|
132
|
+
def infer(self, text):
|
|
133
|
+
key = id(text)
|
|
134
|
+
grams = self._char_counts(text)
|
|
135
|
+
# sublinear tf * idf over the sparse support, then L2 normalise
|
|
136
|
+
acc = {}
|
|
137
|
+
for gram, cnt in grams.items():
|
|
138
|
+
idx = self.vocab.get(gram)
|
|
139
|
+
if idx is None:
|
|
140
|
+
continue
|
|
141
|
+
acc[idx] = (1.0 + math.log(cnt)) * self.idf[idx]
|
|
142
|
+
norm = math.sqrt(sum(v * v for v in acc.values())) or 1.0
|
|
143
|
+
z = self.intercept
|
|
144
|
+
for idx, w in acc.items():
|
|
145
|
+
z += self.coef[idx] * (w / norm)
|
|
146
|
+
p_raw = 1.0 / (1.0 + math.exp(-max(-30.0, min(30.0, z))))
|
|
147
|
+
zz = math.log(max(p_raw, 1e-6) / max(1e-6, 1.0 - p_raw))
|
|
148
|
+
p = 1.0 / (1.0 + math.exp(-max(-30.0, min(30.0,
|
|
149
|
+
self.platt_a * zz + self.platt_b))))
|
|
150
|
+
return round(p, 6)
|
|
151
|
+
|
|
152
|
+
def _char_counts(self, text):
|
|
153
|
+
ck = ('t', text)
|
|
154
|
+
cached = self._norm_cache.get(ck)
|
|
155
|
+
if cached is None:
|
|
156
|
+
cached = _char_wb_ngram_counts(normalize_text(text),
|
|
157
|
+
self.min_n, self.max_n)
|
|
158
|
+
if len(self._norm_cache) < 512:
|
|
159
|
+
self._norm_cache[ck] = cached
|
|
160
|
+
return cached
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def infer_recall_intent(text, head):
|
|
164
|
+
return head.infer(text)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------- dialogue/task/echo
|
|
168
|
+
|
|
169
|
+
def infer_dialogue_act(text, intent_prob):
|
|
170
|
+
tl = str(text).lower()
|
|
171
|
+
if any(k in tl for k in ERR_TOKENS):
|
|
172
|
+
return 'error_report'
|
|
173
|
+
if any(k in tl for k in ACK_TOKENS) and len(tl) <= 12:
|
|
174
|
+
return 'acknowledgement'
|
|
175
|
+
has_interrogative = ('?' in tl or '?' in tl
|
|
176
|
+
or any(k in tl for k in INTERROG))
|
|
177
|
+
recall_ctx = any(k in tl for k in RECALL_CTX)
|
|
178
|
+
if recall_ctx and has_interrogative:
|
|
179
|
+
return 'question'
|
|
180
|
+
if has_interrogative:
|
|
181
|
+
return 'question'
|
|
182
|
+
if any(k in tl for k in REQ_TOKENS):
|
|
183
|
+
return 'request'
|
|
184
|
+
if any(k in tl for k in PLAN_TOKENS):
|
|
185
|
+
return 'planning'
|
|
186
|
+
if intent_prob < 0.40:
|
|
187
|
+
return 'statement'
|
|
188
|
+
return 'other'
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def infer_task_need(dialogue_act):
|
|
192
|
+
return {'error_report': 'required', 'question': 'optional',
|
|
193
|
+
'request': 'optional', 'planning': 'none',
|
|
194
|
+
'acknowledgement': 'none', 'statement': 'none',
|
|
195
|
+
'correction': 'none', 'other': 'none'}[dialogue_act]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def compute_echo_risk(containment, dense_top, mark_zero, intent_prob,
|
|
199
|
+
policy):
|
|
200
|
+
ev = policy['echoVeto']
|
|
201
|
+
arms = {'containmentArm': containment >= ev['containmentArm'],
|
|
202
|
+
'denseTopArm': dense_top >= ev['denseTopArm'],
|
|
203
|
+
'markZero': bool(mark_zero),
|
|
204
|
+
'intentBelowCap': intent_prob < ev['requiresIntentBelow']}
|
|
205
|
+
hit = ((arms['containmentArm'] or arms['denseTopArm'])
|
|
206
|
+
and arms['markZero'] and arms['intentBelowCap'])
|
|
207
|
+
return {'arms': arms, 'hit': hit}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def compute_completeness(text, lexicon, required_hint=None,
|
|
211
|
+
resolved_count=None):
|
|
212
|
+
"""Phase-1 conservative proxy. requiredTargetCount is heuristic (no
|
|
213
|
+
gold knowledge at runtime); resolvedTargetCount is an environment
|
|
214
|
+
input (count of expected targets resolved by retrieval) and stays
|
|
215
|
+
None in production shadow until a coverage signal exists."""
|
|
216
|
+
tl = str(text).lower()
|
|
217
|
+
kw = any(k in tl for k in lexicon)
|
|
218
|
+
required = int(required_hint) if required_hint is not None else (2 if kw else 1)
|
|
219
|
+
status = 'unknown' if kw else 'complete'
|
|
220
|
+
return {'requiredTargetCount': required,
|
|
221
|
+
'resolvedTargetCount': resolved_count,
|
|
222
|
+
'status': status}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def compute_lane(intent_prob, policy):
|
|
226
|
+
return 'explicit' if intent_prob >= policy['thresholds']['tauLane'] \
|
|
227
|
+
else 'proactive'
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------- policy load
|
|
231
|
+
|
|
232
|
+
def load_and_verify_policy(intent_path, policy_path):
|
|
233
|
+
"""Load both artifacts and fail closed on any inconsistency."""
|
|
234
|
+
with open(intent_path, encoding='utf-8') as f:
|
|
235
|
+
ip = json.load(f)
|
|
236
|
+
with open(policy_path, encoding='utf-8') as f:
|
|
237
|
+
ap = json.load(f)
|
|
238
|
+
need_ip = ('policyVersion', 'goldDigest', 'runId', 'configHash',
|
|
239
|
+
'featureSchema', 'vocabulary', 'idf', 'coefficients',
|
|
240
|
+
'intercept', 'calibration')
|
|
241
|
+
need_ap = ('policyVersion', 'goldDigest', 'runId', 'configHash', 'mode',
|
|
242
|
+
'thresholds', 'decisionOrder', 'echoVeto', 'completenessGate',
|
|
243
|
+
'hardGates', 'reasonCodes')
|
|
244
|
+
for k in need_ip:
|
|
245
|
+
if k not in ip:
|
|
246
|
+
raise ValueError('intent policy missing field: %s' % k)
|
|
247
|
+
for k in need_ap:
|
|
248
|
+
if k not in ap:
|
|
249
|
+
raise ValueError('activation policy missing field: %s' % k)
|
|
250
|
+
if ip['goldDigest'] != ap['goldDigest'] or ip['runId'] != ap['runId']:
|
|
251
|
+
raise ValueError('intent/activation policy provenance mismatch')
|
|
252
|
+
L = len(ip['vocabulary'])
|
|
253
|
+
if not (L == len(ip['idf']) == len(ip['coefficients'])):
|
|
254
|
+
raise ValueError('vocab/idf/coefficient length mismatch')
|
|
255
|
+
for name, doc in (('intent', ip), ('activation', ap)):
|
|
256
|
+
probe = {k: v for k, v in doc.items() if k != 'configHash'}
|
|
257
|
+
payload = json.dumps(probe, sort_keys=True, ensure_ascii=False)
|
|
258
|
+
expect = 'cfgh_' + hashlib.sha256(payload.encode('utf-8')).hexdigest()[:32]
|
|
259
|
+
if expect != doc['configHash']:
|
|
260
|
+
raise ValueError('%s configHash mismatch: %s vs %s'
|
|
261
|
+
% (name, expect, doc.get('configHash')))
|
|
262
|
+
if ap['mode'] != 'shadow-candidate':
|
|
263
|
+
raise ValueError('refusing non-shadow mode in round-1 runtime')
|
|
264
|
+
head = RecallIntentHead(ip)
|
|
265
|
+
return {'head': head, 'policy': ap}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------ decision core
|
|
269
|
+
|
|
270
|
+
def decide_activation_v2(features, head, policy):
|
|
271
|
+
"""features keys:
|
|
272
|
+
text raw query text
|
|
273
|
+
denseTop top-1 fused/dense similarity of ranked candidates
|
|
274
|
+
margin denseTop - second dense (or 1.0 when single)
|
|
275
|
+
containment lexical containment query->top1 candidate text
|
|
276
|
+
mark 0/1 interrogative-or-recall-marker present
|
|
277
|
+
nCand number of ranked candidates in view
|
|
278
|
+
candidateHit env input: expected/relevant target present in top-K
|
|
279
|
+
(shadow eval: gold match; production: memoryRefs
|
|
280
|
+
overlap or future coverage signal; default False)
|
|
281
|
+
resolvedTargets env input: count of expected targets resolved
|
|
282
|
+
requiredHint optional completeness hint
|
|
283
|
+
repetition optional logging-only counters {mentions, failures}
|
|
284
|
+
hardGates dict of booleans: harmful/correction/ignored/stale/
|
|
285
|
+
wrongScope/piiHigh (absent => False)
|
|
286
|
+
Returns dict: lane, decision, reasonCodes, features snapshot."""
|
|
287
|
+
th = policy['thresholds']
|
|
288
|
+
reason = []
|
|
289
|
+
hg = features.get('hardGates') or {}
|
|
290
|
+
if any(bool(hg.get(k)) for k in
|
|
291
|
+
('harmful', 'correction', 'ignored', 'stale', 'wrongScope')):
|
|
292
|
+
hit_gate = next((k for k in ('piiHigh', 'wrongScope', 'stale',
|
|
293
|
+
'ignored', 'correction', 'harmful')
|
|
294
|
+
if hg.get(k)), 'harmful')
|
|
295
|
+
return _pack(features, policy, None, 'suppress',
|
|
296
|
+
['hard_gate_%s' % hit_gate])
|
|
297
|
+
if hg.get('piiHigh'):
|
|
298
|
+
return _pack(features, policy, None, 'suppress', ['hard_gate_pii'])
|
|
299
|
+
intent = infer_recall_intent(features['text'], head)
|
|
300
|
+
dact = infer_dialogue_act(features['text'], intent)
|
|
301
|
+
tneed = infer_task_need(dact)
|
|
302
|
+
echo = compute_echo_risk(features['containment'], features['denseTop'],
|
|
303
|
+
features['mark'] == 0, intent, policy)
|
|
304
|
+
comp = compute_completeness(features['text'],
|
|
305
|
+
policy['completenessGate']['lexicon'],
|
|
306
|
+
required_hint=features.get('requiredHint'),
|
|
307
|
+
resolved_count=features.get('resolvedTargets'))
|
|
308
|
+
lane = compute_lane(intent, policy)
|
|
309
|
+
hit = bool(features.get('candidateHit'))
|
|
310
|
+
margin = float(features.get('margin') or 0.0)
|
|
311
|
+
|
|
312
|
+
def finish(decision, extra=()):
|
|
313
|
+
snap = {'intentProb': intent, 'dialogueAct': dact, 'taskNeed': tneed,
|
|
314
|
+
'echoRisk': echo, 'completeness': comp, 'lane': lane,
|
|
315
|
+
'margin': margin}
|
|
316
|
+
rep = features.get('repetition') or {}
|
|
317
|
+
snap['repetitionLogged'] = rep
|
|
318
|
+
return _pack(features, policy, snap, decision, list(reason) + list(extra))
|
|
319
|
+
|
|
320
|
+
if lane == 'explicit':
|
|
321
|
+
if intent >= th['tauHi'] and hit:
|
|
322
|
+
if margin >= th['deltaExp'] and comp['status'] == 'complete':
|
|
323
|
+
return finish('emit', ['explicit_lane',
|
|
324
|
+
'completeness_complete'])
|
|
325
|
+
if margin >= th['deltaExp']:
|
|
326
|
+
return finish('prefetch',
|
|
327
|
+
['explicit_lane',
|
|
328
|
+
'completeness_%s' % comp['status']])
|
|
329
|
+
return finish('prefetch', ['explicit_lane', 'margin_below_delta'])
|
|
330
|
+
if intent >= th['tauLo'] and hit:
|
|
331
|
+
return finish('prefetch', ['explicit_lane_weak'])
|
|
332
|
+
# fall through: explicit lane without hit behaves like proactive
|
|
333
|
+
# relevance check minus echo suppression rights
|
|
334
|
+
if (margin >= th['deltaPro'] and features.get('nCand', 0) >= 2
|
|
335
|
+
and intent < 0.35
|
|
336
|
+
and features['denseTop'] < policy['echoVeto']['denseTopArm']):
|
|
337
|
+
return finish('prefetch', ['proactive_margin_fallback'])
|
|
338
|
+
return finish('suppress', ['suppress_low_signal'])
|
|
339
|
+
# proactive lane
|
|
340
|
+
if echo['hit']:
|
|
341
|
+
return finish('suppress', ['echo_veto_proactive'])
|
|
342
|
+
if margin >= th['deltaPro'] and features.get('nCand', 0) >= 2 \
|
|
343
|
+
and features['denseTop'] < policy['echoVeto']['denseTopArm']:
|
|
344
|
+
return finish('prefetch', ['proactive_margin'])
|
|
345
|
+
return finish('suppress', ['suppress_low_signal'])
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _pack(features, policy, snapshot, decision, reason_codes):
|
|
349
|
+
out = {
|
|
350
|
+
'featurePolicyVersion': FEATURES_POLICY_VERSION,
|
|
351
|
+
'activationPolicyVersion': ACTIVATION_POLICY_VERSION,
|
|
352
|
+
'decision': decision,
|
|
353
|
+
'reasonCodes': list(reason_codes),
|
|
354
|
+
'advisoryOnly': None,
|
|
355
|
+
'requiresCrossWorkspaceRelay': bool(features.get('requiresRelayFlag')),
|
|
356
|
+
'piiClass': features.get('piiClass'),
|
|
357
|
+
'note': 'round-1 shadow only; cross-workspace/PII tiers are JS '
|
|
358
|
+
'authority layers (fail closed without explicit policy)',
|
|
359
|
+
}
|
|
360
|
+
if snapshot is not None:
|
|
361
|
+
out['features'] = snapshot
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def replay_features_v2(rows, head, policy):
|
|
366
|
+
"""Batch helper for offline replay/parity: rows carry precomputed
|
|
367
|
+
retrieval features; returns per-row decision dicts."""
|
|
368
|
+
return [dict(decide_activation_v2(r, head, policy),
|
|
369
|
+
sampleId=r.get('id')) for r in rows]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# --------------------------------------------------------------- self-test
|
|
373
|
+
|
|
374
|
+
if __name__ == '__main__':
|
|
375
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
376
|
+
pol_dir = os.path.join(here, 'policies')
|
|
377
|
+
loaded = load_and_verify_policy(
|
|
378
|
+
os.path.join(pol_dir, 'recall_intent_lr_v1.json'),
|
|
379
|
+
os.path.join(pol_dir, 'activation_policy_v2.json'))
|
|
380
|
+
demo_rows = [
|
|
381
|
+
{'id': 'demo-recall', 'text': '之前为什么选 BGE-M3?',
|
|
382
|
+
'denseTop': 0.72, 'margin': 0.10, 'containment': 0.55, 'mark': 1,
|
|
383
|
+
'nCand': 5, 'candidateHit': True},
|
|
384
|
+
{'id': 'demo-echo', 'text': '中午那碗面条挺不错的。',
|
|
385
|
+
'denseTop': 0.83, 'margin': 0.30, 'containment': 0.22, 'mark': 0,
|
|
386
|
+
'nCand': 4, 'candidateHit': False},
|
|
387
|
+
]
|
|
388
|
+
for r in demo_rows:
|
|
389
|
+
out = decide_activation_v2(r, loaded['head'], loaded['policy'])
|
|
390
|
+
print(r['id'], out['decision'], out['reasonCodes'],
|
|
391
|
+
'intent=%.4f' % out['features']['intentProb'])
|
|
392
|
+
print('SELFTEST OK')
|