dsh-biocore 0.1.4 → 0.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/lib/index.js +7 -1
- package/lib/modules/core-turn.js +36 -2
- package/lib/modules/sanitize-reply.js +60 -0
- package/package.json +1 -1
- package/python/biocore_runtime/pipeline.py +34 -1
- package/ui/panel.html +1 -0
- package/ui/panel.js +1 -0
package/lib/index.js
CHANGED
|
@@ -167,6 +167,7 @@ export function readFullConfig(dataDir) {
|
|
|
167
167
|
embedding: storedEmbedding(source),
|
|
168
168
|
rerank: normalizeRerank(source.rerank),
|
|
169
169
|
expression: normalizeExpression(source.expression),
|
|
170
|
+
sanitize_reply: source.sanitize_reply !== false,
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -176,6 +177,7 @@ function writeFullConfig(file, config) {
|
|
|
176
177
|
embedding: config.embedding,
|
|
177
178
|
rerank: config.rerank,
|
|
178
179
|
expression: config.expression,
|
|
180
|
+
sanitize_reply: config.sanitize_reply !== false,
|
|
179
181
|
}, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
180
182
|
try { chmodSync(file, 0o600) } catch { /* Windows may ignore POSIX modes */ }
|
|
181
183
|
}
|
|
@@ -191,6 +193,7 @@ export async function handleSettings(req, res, dataDir) {
|
|
|
191
193
|
embedding: maskedEmbedding(config.embedding),
|
|
192
194
|
rerank: maskedRerank(config.rerank),
|
|
193
195
|
expression: config.expression,
|
|
196
|
+
sanitize_reply: config.sanitize_reply !== false,
|
|
194
197
|
restart_required: false,
|
|
195
198
|
})
|
|
196
199
|
}
|
|
@@ -213,7 +216,8 @@ export async function handleSettings(req, res, dataDir) {
|
|
|
213
216
|
const hasEmbedding = body.embedding && typeof body.embedding === 'object'
|
|
214
217
|
const hasRerank = body.rerank && typeof body.rerank === 'object'
|
|
215
218
|
const hasExpression = body.expression && typeof body.expression === 'object'
|
|
216
|
-
|
|
219
|
+
const hasSanitize = typeof body.sanitize_reply === 'boolean'
|
|
220
|
+
if (!hasEmbedding && !hasRerank && !hasExpression && !hasSanitize) {
|
|
217
221
|
return sendJson(res, 400, { ok: false, reason: 'no_settings' })
|
|
218
222
|
}
|
|
219
223
|
if (hasEmbedding) {
|
|
@@ -236,12 +240,14 @@ export async function handleSettings(req, res, dataDir) {
|
|
|
236
240
|
config.rerank = rerank
|
|
237
241
|
}
|
|
238
242
|
if (hasExpression) config.expression = normalizeExpression(body.expression)
|
|
243
|
+
if (hasSanitize) config.sanitize_reply = body.sanitize_reply === true
|
|
239
244
|
writeFullConfig(paths.file, config)
|
|
240
245
|
return sendJson(res, 200, {
|
|
241
246
|
ok: true,
|
|
242
247
|
embedding: maskedEmbedding(config.embedding),
|
|
243
248
|
rerank: maskedRerank(config.rerank),
|
|
244
249
|
expression: config.expression,
|
|
250
|
+
sanitize_reply: config.sanitize_reply !== false,
|
|
245
251
|
restart_required: true,
|
|
246
252
|
hint: '重启 BioCore 生效',
|
|
247
253
|
})
|
package/lib/modules/core-turn.js
CHANGED
|
@@ -26,6 +26,11 @@ import { applyGateText, assistantTextOf } from './safety-gate.js'
|
|
|
26
26
|
import { defaultStoreDir } from './trace.js'
|
|
27
27
|
import { runtimeAction, runTurn } from '../py-bridge.js'
|
|
28
28
|
import { EXPRESSION_DEFAULTS, readExpressionSettings } from './expression-config.js'
|
|
29
|
+
import {
|
|
30
|
+
readSanitizeReplySetting,
|
|
31
|
+
sanitizeReply,
|
|
32
|
+
sanitizeReplyEnabled,
|
|
33
|
+
} from './sanitize-reply.js'
|
|
29
34
|
|
|
30
35
|
export const TURN_CONTEXT_NAME = 'biocore-expression-turn'
|
|
31
36
|
export const TURN_CONTEXT_ORDER = -190
|
|
@@ -89,6 +94,17 @@ export function diagnosticsFromState(state, userText) {
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/** Final assistant text: core clean_text wins, JS sanitizer is the fallback.
|
|
98
|
+
* sanitizeEnabled=false keeps the original (user disabled the switch). */
|
|
99
|
+
export function chooseCleanReply(original, coreCleanText, options) {
|
|
100
|
+
const opts = options || {}
|
|
101
|
+
const source = String(original == null ? '' : original)
|
|
102
|
+
if (opts.sanitizeEnabled === false) return source
|
|
103
|
+
if (typeof coreCleanText === 'string') return coreCleanText
|
|
104
|
+
const sanitize = typeof opts.sanitize === 'function' ? opts.sanitize : sanitizeReply
|
|
105
|
+
return String(sanitize(source))
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
export function turnStartPayload(options) {
|
|
93
109
|
const opts = options || {}
|
|
94
110
|
const expression = opts.expression && typeof opts.expression === 'object'
|
|
@@ -118,6 +134,7 @@ export function turnEndPayload(options) {
|
|
|
118
134
|
store: opts.store || defaultStoreDir(),
|
|
119
135
|
capture: false,
|
|
120
136
|
memoryEnabled: false,
|
|
137
|
+
sanitize_reply: opts.sanitizeReply !== false,
|
|
121
138
|
diagnostics,
|
|
122
139
|
}, diagnostics, {
|
|
123
140
|
modules_trace: diagnostics.modules,
|
|
@@ -135,6 +152,9 @@ export function makeCoreSessionListener(options) {
|
|
|
135
152
|
const getExpression = typeof opts.getExpression === 'function'
|
|
136
153
|
? opts.getExpression
|
|
137
154
|
: () => readExpressionSettings(stateDir)
|
|
155
|
+
const getSanitizeReply = typeof opts.getSanitizeReply === 'function'
|
|
156
|
+
? opts.getSanitizeReply
|
|
157
|
+
: () => readSanitizeReplySetting(stateDir)
|
|
138
158
|
const store = opts.store || defaultStoreDir()
|
|
139
159
|
const log = typeof opts.log === 'function' ? opts.log : (line) => {
|
|
140
160
|
try { console.error(line) } catch { /* noop */ }
|
|
@@ -186,9 +206,11 @@ export function makeCoreSessionListener(options) {
|
|
|
186
206
|
}).catch(() => null)
|
|
187
207
|
return
|
|
188
208
|
}
|
|
189
|
-
// assistant/message: gate + trace via one turn_end call.
|
|
209
|
+
// assistant/message: sanitize -> gate + trace via one turn_end call.
|
|
190
210
|
const original = assistantTextOf(event)
|
|
191
211
|
if (!original) return
|
|
212
|
+
const sanitizeEnabled = sanitizeReplyEnabled(getSanitizeReply())
|
|
213
|
+
const cleaned = sanitizeEnabled ? sanitizeReply(original) : original
|
|
192
214
|
const userText = lastUser.get(sessionId) || ''
|
|
193
215
|
void (async () => {
|
|
194
216
|
try {
|
|
@@ -198,14 +220,23 @@ export function makeCoreSessionListener(options) {
|
|
|
198
220
|
const state = read && !read.error && !read.missing ? read.state : {}
|
|
199
221
|
const payload = turnEndPayload({
|
|
200
222
|
sessionId,
|
|
201
|
-
assistantText:
|
|
223
|
+
assistantText: cleaned,
|
|
202
224
|
userText,
|
|
203
225
|
switches,
|
|
204
226
|
store,
|
|
205
227
|
state,
|
|
228
|
+
sanitizeReply: sanitizeEnabled,
|
|
206
229
|
})
|
|
207
230
|
const result = await runAction('turn_end', payload)
|
|
208
231
|
if (!result || result.ok !== true) return
|
|
232
|
+
const reply = result.reply && typeof result.reply === 'object' ? result.reply : {}
|
|
233
|
+
const coreCleanText = typeof reply.clean_text === 'string'
|
|
234
|
+
? reply.clean_text
|
|
235
|
+
: (typeof result.clean_text === 'string' ? result.clean_text : null)
|
|
236
|
+
const finalText = chooseCleanReply(original, coreCleanText, {
|
|
237
|
+
sanitizeEnabled,
|
|
238
|
+
sanitize: sanitizeReply,
|
|
239
|
+
})
|
|
209
240
|
const gate = result.gate && typeof result.gate === 'object' ? result.gate : {}
|
|
210
241
|
const action = stringId(gate.action || 'pass')
|
|
211
242
|
if ((action === 'replace' || action === 'truncate')
|
|
@@ -216,6 +247,9 @@ export function makeCoreSessionListener(options) {
|
|
|
216
247
|
+ ' blocked=' + (gate.blocked === true ? '1' : '0')
|
|
217
248
|
+ ' rule=' + stringId(gate.rule || '')
|
|
218
249
|
+ ' chars=' + original.length + '->' + gate.text.length)
|
|
250
|
+
} else if (finalText !== original) {
|
|
251
|
+
// No gate rewrite: write the core-cleaned / JS-fallback text back.
|
|
252
|
+
applyGateText(event, finalText)
|
|
219
253
|
}
|
|
220
254
|
if (result.trace_file) {
|
|
221
255
|
logLine(log, CORE_TRACE_PREFIX + ' session=' + sessionId
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Strip BioCore output markers from assistant replies (task-223).
|
|
2
|
+
*
|
|
3
|
+
* Removes:
|
|
4
|
+
* - <bc>...</bc> and <biocore>...</biocore> blocks (also unclosed to EOL/EOS)
|
|
5
|
+
* - bare compact JSON objects that only carry e/i[/n] keys
|
|
6
|
+
* - extra whitespace; empty result stays empty (never falls back to input)
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_SANITIZE_REPLY = true
|
|
12
|
+
|
|
13
|
+
const TAG_RE = /<(bc|biocore)\b[^>]*>/gi
|
|
14
|
+
const BARE_JSON_RE = /\{\s*"e"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*,\s*"i"\s*:\s*-?\d+(?:\.\d+)?(?:\s*,\s*"n"\s*:\s*-?\d+(?:\.\d+)?)?\s*\}/g
|
|
15
|
+
|
|
16
|
+
function stripClosedTags(text) {
|
|
17
|
+
return String(text)
|
|
18
|
+
.replace(/<bc\b[^>]*>[\s\S]*?<\/bc\s*>/gi, '')
|
|
19
|
+
.replace(/<biocore\b[^>]*>[\s\S]*?<\/biocore\s*>/gi, '')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stripUnclosedTags(text) {
|
|
23
|
+
// Until line end or a Chinese/ASCII sentence terminator; the terminator stays.
|
|
24
|
+
return String(text)
|
|
25
|
+
.replace(/<bc\b[^>]*>[\s\S]*?(?:[\n。!?!?]|$)/gi, '')
|
|
26
|
+
.replace(/<biocore\b[^>]*>[\s\S]*?(?:[\n。!?!?]|$)/gi, '')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeWhitespace(text) {
|
|
30
|
+
return String(text)
|
|
31
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
32
|
+
.replace(/[ \t]*\n[ \t]*/g, '\n')
|
|
33
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
34
|
+
.trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Main entry: returns the cleaned reply (may be ''); never reverts to input. */
|
|
38
|
+
export function sanitizeReply(text, options) {
|
|
39
|
+
const opts = options || {}
|
|
40
|
+
if (opts.enabled === false) return String(text == null ? '' : text)
|
|
41
|
+
const source = String(text == null ? '' : text)
|
|
42
|
+
let out = stripClosedTags(source)
|
|
43
|
+
out = stripUnclosedTags(out)
|
|
44
|
+
out = out.replace(BARE_JSON_RE, '')
|
|
45
|
+
return normalizeWhitespace(out)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Config.json top-level flag: only an explicit false disables cleaning. */
|
|
49
|
+
export function readSanitizeReplySetting(dataDir) {
|
|
50
|
+
try {
|
|
51
|
+
const doc = JSON.parse(readFileSync(join(String(dataDir || '').trim(), 'config.json'), 'utf8'))
|
|
52
|
+
if (doc && typeof doc === 'object' && doc.sanitize_reply === false) return false
|
|
53
|
+
} catch { /* missing/bad config -> default on */ }
|
|
54
|
+
return DEFAULT_SANITIZE_REPLY
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Payload helper: sanitize_reply defaults to true unless explicitly false. */
|
|
58
|
+
export function sanitizeReplyEnabled(value) {
|
|
59
|
+
return value !== false
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -96,6 +96,11 @@ try:
|
|
|
96
96
|
except Exception: # pragma: no cover
|
|
97
97
|
_rerank_mod = None
|
|
98
98
|
|
|
99
|
+
try:
|
|
100
|
+
from biocore_model import sanitize as _sanitize
|
|
101
|
+
except Exception: # pragma: no cover
|
|
102
|
+
_sanitize = None
|
|
103
|
+
|
|
99
104
|
try: # self-layer modules (all pure; optional so a stripped checkout still runs)
|
|
100
105
|
from biocore_model import x_directives as _x_directives
|
|
101
106
|
from biocore_model import self_intent as _self_intent
|
|
@@ -1794,10 +1799,35 @@ def dispatch(action, payload=None):
|
|
|
1794
1799
|
return bad("missing session_id")
|
|
1795
1800
|
if "assistant_text" not in data:
|
|
1796
1801
|
return bad("missing assistant_text")
|
|
1797
|
-
|
|
1802
|
+
raw_assistant = clean_text(data.get("assistant_text"))
|
|
1803
|
+
do_sanitize = _as_bool(data.get("sanitize_reply"), True)
|
|
1804
|
+
cleaned = raw_assistant
|
|
1805
|
+
sanitized = False
|
|
1806
|
+
removed_tags = 0
|
|
1807
|
+
if do_sanitize and _sanitize is not None:
|
|
1808
|
+
stats = getattr(_sanitize, "STATS", {}) or {}
|
|
1809
|
+
before = (int(stats.get("tag_stripped", 0) or 0),
|
|
1810
|
+
int(stats.get("json_stripped", 0) or 0),
|
|
1811
|
+
int(stats.get("fenced_stripped", 0) or 0))
|
|
1812
|
+
try:
|
|
1813
|
+
cleaned = _sanitize.clean(raw_assistant)
|
|
1814
|
+
except Exception:
|
|
1815
|
+
cleaned = raw_assistant
|
|
1816
|
+
after = (int(stats.get("tag_stripped", 0) or 0),
|
|
1817
|
+
int(stats.get("json_stripped", 0) or 0),
|
|
1818
|
+
int(stats.get("fenced_stripped", 0) or 0))
|
|
1819
|
+
removed_tags = max(0, sum(after) - sum(before))
|
|
1820
|
+
sanitized = cleaned != raw_assistant or removed_tags > 0
|
|
1821
|
+
assistant_text = cleaned
|
|
1798
1822
|
switches = normalize_switches(data.get("switches"))
|
|
1799
1823
|
gate = gate_reply(assistant_text, {
|
|
1800
1824
|
"switches": switches, "user_text": data.get("user_text", "")})
|
|
1825
|
+
if do_sanitize and _sanitize is not None:
|
|
1826
|
+
try:
|
|
1827
|
+
gate = dict(gate)
|
|
1828
|
+
gate["text"] = _sanitize.clean(gate.get("text") or "")
|
|
1829
|
+
except Exception:
|
|
1830
|
+
pass
|
|
1801
1831
|
captured = capture(session_id, "assistant", assistant_text, store, {
|
|
1802
1832
|
"enabled": _as_bool(data.get("memoryEnabled",
|
|
1803
1833
|
switches.get("memoryEnabled", True)),
|
|
@@ -1818,6 +1848,9 @@ def dispatch(action, payload=None):
|
|
|
1818
1848
|
"captured": {"captured": bool(decision.get("capture")),
|
|
1819
1849
|
"entry_id": str(entry.get("id") or ""),
|
|
1820
1850
|
"reason": str(decision.get("reason") or "")},
|
|
1851
|
+
"clean_text": cleaned,
|
|
1852
|
+
"sanitized": bool(sanitized),
|
|
1853
|
+
"removed_tags": int(removed_tags),
|
|
1821
1854
|
"trace": trace,
|
|
1822
1855
|
"trace_file": trace_file,
|
|
1823
1856
|
}
|
package/ui/panel.html
CHANGED
|
@@ -247,6 +247,7 @@
|
|
|
247
247
|
<dl class="advanced-info">
|
|
248
248
|
<div><dt>数据目录</dt><dd id="dataDirText">宿主托管</dd></div>
|
|
249
249
|
<div><dt>插件版本</dt><dd id="pluginVersion">0.1.0</dd></div>
|
|
250
|
+
<div><dt>出口清洗</dt><dd id="sanitizeReplyText">开</dd></div>
|
|
250
251
|
</dl>
|
|
251
252
|
<div class="advanced-actions">
|
|
252
253
|
<button id="resetLayout" class="advanced-button" type="button">恢复默认布局</button>
|