@a9i5k4/dsh-auto-memory 2.1.4 → 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.
@@ -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')