dsh-biocore 0.1.3 → 0.1.5

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.
Files changed (212) hide show
  1. package/lib/index.js +176 -27
  2. package/lib/modules/core-turn.js +17 -2
  3. package/lib/modules/sanitize-reply.js +60 -0
  4. package/package.json +1 -1
  5. package/python/biocore_runtime/pipeline.py +42 -0
  6. package/python/biocore_runtime/rerank.py +201 -0
  7. package/python/mindbase/__init__.py +18 -0
  8. package/python/mindbase/__main__.py +3 -0
  9. package/python/mindbase/actions.py +56 -0
  10. package/python/mindbase/active_inference.py +54 -0
  11. package/python/mindbase/addressing.py +69 -0
  12. package/python/mindbase/affect.py +86 -0
  13. package/python/mindbase/affective.py +111 -0
  14. package/python/mindbase/allostasis.py +28 -0
  15. package/python/mindbase/appraisal.py +101 -0
  16. package/python/mindbase/attachment.py +49 -0
  17. package/python/mindbase/attention.py +30 -0
  18. package/python/mindbase/autonomy.py +81 -0
  19. package/python/mindbase/autonomy_bench.py +251 -0
  20. package/python/mindbase/autonomy_evolve.py +342 -0
  21. package/python/mindbase/autonomy_loop.py +65 -0
  22. package/python/mindbase/autonomy_meta.py +91 -0
  23. package/python/mindbase/base_only.py +28 -0
  24. package/python/mindbase/benchmark.py +61 -0
  25. package/python/mindbase/bio_adaptation.py +274 -0
  26. package/python/mindbase/bio_noise.py +39 -0
  27. package/python/mindbase/bio_semantic.py +138 -0
  28. package/python/mindbase/bm25.py +46 -0
  29. package/python/mindbase/bootstrap.py +256 -0
  30. package/python/mindbase/bootstrap_real.py +231 -0
  31. package/python/mindbase/brain.py +178 -0
  32. package/python/mindbase/care.py +78 -0
  33. package/python/mindbase/chaos_search.py +79 -0
  34. package/python/mindbase/character_profile.py +33 -0
  35. package/python/mindbase/circadian.py +38 -0
  36. package/python/mindbase/circuit.py +235 -0
  37. package/python/mindbase/clock.py +53 -0
  38. package/python/mindbase/companion_runtime.py +366 -0
  39. package/python/mindbase/companion_train.py +126 -0
  40. package/python/mindbase/config.py +66 -0
  41. package/python/mindbase/connection_state.py +76 -0
  42. package/python/mindbase/connectome_prior.py +35 -0
  43. package/python/mindbase/connectome_world.py +158 -0
  44. package/python/mindbase/constructs.py +88 -0
  45. package/python/mindbase/context_builder.py +58 -0
  46. package/python/mindbase/critical.py +36 -0
  47. package/python/mindbase/criticality.py +123 -0
  48. package/python/mindbase/defaults.py +27 -0
  49. package/python/mindbase/demo.py +163 -0
  50. package/python/mindbase/diary.py +116 -0
  51. package/python/mindbase/diary_service.py +40 -0
  52. package/python/mindbase/digital_prior.py +37 -0
  53. package/python/mindbase/diversity.py +110 -0
  54. package/python/mindbase/drives.py +106 -0
  55. package/python/mindbase/embedding.py +132 -0
  56. package/python/mindbase/embedding_memory.py +74 -0
  57. package/python/mindbase/embodiment.py +83 -0
  58. package/python/mindbase/emotion_calibration.py +39 -0
  59. package/python/mindbase/emotion_calibrator.py +50 -0
  60. package/python/mindbase/emotion_core.py +113 -0
  61. package/python/mindbase/emotion_dynamics.py +104 -0
  62. package/python/mindbase/emotion_intent.py +164 -0
  63. package/python/mindbase/emotion_layer.py +200 -0
  64. package/python/mindbase/emotion_schema.py +92 -0
  65. package/python/mindbase/engine.py +781 -0
  66. package/python/mindbase/episode_log.py +90 -0
  67. package/python/mindbase/evaluation.py +493 -0
  68. package/python/mindbase/event_appraisal.py +84 -0
  69. package/python/mindbase/evolution.py +77 -0
  70. package/python/mindbase/evolve_offline.py +232 -0
  71. package/python/mindbase/experiments.py +358 -0
  72. package/python/mindbase/expression.py +54 -0
  73. package/python/mindbase/external_brain.py +157 -0
  74. package/python/mindbase/fly_mb.py +159 -0
  75. package/python/mindbase/fly_tasks.py +45 -0
  76. package/python/mindbase/forage.py +179 -0
  77. package/python/mindbase/goal_generator.py +98 -0
  78. package/python/mindbase/goals.py +153 -0
  79. package/python/mindbase/grid_world.py +123 -0
  80. package/python/mindbase/group_autonomy.py +92 -0
  81. package/python/mindbase/group_context.py +67 -0
  82. package/python/mindbase/homeostatic.py +71 -0
  83. package/python/mindbase/homeostatic_action.py +38 -0
  84. package/python/mindbase/human_standard.py +53 -0
  85. package/python/mindbase/identity_suite.py +50 -0
  86. package/python/mindbase/image_context.py +27 -0
  87. package/python/mindbase/impression.py +119 -0
  88. package/python/mindbase/initiative_quota.py +145 -0
  89. package/python/mindbase/insight.py +51 -0
  90. package/python/mindbase/intelligence_search.py +108 -0
  91. package/python/mindbase/intention.py +76 -0
  92. package/python/mindbase/interaction_log.py +30 -0
  93. package/python/mindbase/interoception.py +67 -0
  94. package/python/mindbase/invariants.py +53 -0
  95. package/python/mindbase/iteration.py +29 -0
  96. package/python/mindbase/layered_memory.py +170 -0
  97. package/python/mindbase/layers/__init__.py +18 -0
  98. package/python/mindbase/layers/l0_substrate.py +39 -0
  99. package/python/mindbase/layers/l1_homeostasis.py +6 -0
  100. package/python/mindbase/layers/l2_affect.py +6 -0
  101. package/python/mindbase/layers/l3_attention.py +6 -0
  102. package/python/mindbase/layers/l4_learning.py +17 -0
  103. package/python/mindbase/layers/l5_self_social.py +35 -0
  104. package/python/mindbase/layers/l6_expression.py +23 -0
  105. package/python/mindbase/layers/l7_evaluation.py +45 -0
  106. package/python/mindbase/learner.py +196 -0
  107. package/python/mindbase/lexicon.py +33 -0
  108. package/python/mindbase/life_state.py +169 -0
  109. package/python/mindbase/lifecycle.py +87 -0
  110. package/python/mindbase/liquid_readout.py +48 -0
  111. package/python/mindbase/llm_adapter.py +233 -0
  112. package/python/mindbase/llm_cache.py +77 -0
  113. package/python/mindbase/llm_gate.py +85 -0
  114. package/python/mindbase/llm_openai.py +130 -0
  115. package/python/mindbase/llm_setup.py +99 -0
  116. package/python/mindbase/longrun.py +83 -0
  117. package/python/mindbase/longterm_memory.py +606 -0
  118. package/python/mindbase/mb/__init__.py +14 -0
  119. package/python/mindbase/mb/apl.py +22 -0
  120. package/python/mindbase/mb/dan.py +14 -0
  121. package/python/mindbase/mb/habituation.py +27 -0
  122. package/python/mindbase/mb/kc.py +28 -0
  123. package/python/mindbase/mb/mbon.py +23 -0
  124. package/python/mindbase/mb/mushroom_body.py +116 -0
  125. package/python/mindbase/memory.py +201 -0
  126. package/python/mindbase/memory_framework.py +158 -0
  127. package/python/mindbase/memory_retriever.py +110 -0
  128. package/python/mindbase/memory_summarizer.py +177 -0
  129. package/python/mindbase/memory_task.py +32 -0
  130. package/python/mindbase/memory_trace.py +46 -0
  131. package/python/mindbase/meta.py +32 -0
  132. package/python/mindbase/motivation.py +31 -0
  133. package/python/mindbase/multi_session.py +19 -0
  134. package/python/mindbase/narrative.py +87 -0
  135. package/python/mindbase/needs.py +77 -0
  136. package/python/mindbase/neural.py +45 -0
  137. package/python/mindbase/neuron_models.py +134 -0
  138. package/python/mindbase/noise.py +309 -0
  139. package/python/mindbase/one_on_one.py +60 -0
  140. package/python/mindbase/online_iteration.py +63 -0
  141. package/python/mindbase/ostracism.py +35 -0
  142. package/python/mindbase/parallel_eval.py +60 -0
  143. package/python/mindbase/parallel_qd.py +64 -0
  144. package/python/mindbase/persona.py +221 -0
  145. package/python/mindbase/persona_ab.py +64 -0
  146. package/python/mindbase/persona_bridge.py +129 -0
  147. package/python/mindbase/persona_compile.py +34 -0
  148. package/python/mindbase/persona_dsh.py +120 -0
  149. package/python/mindbase/persona_learner.py +157 -0
  150. package/python/mindbase/physiology.py +197 -0
  151. package/python/mindbase/population_lifecycle.py +83 -0
  152. package/python/mindbase/preferences.py +50 -0
  153. package/python/mindbase/proactive_agent.py +274 -0
  154. package/python/mindbase/proactive_gate.py +53 -0
  155. package/python/mindbase/proactive_group.py +119 -0
  156. package/python/mindbase/proactive_policy.py +422 -0
  157. package/python/mindbase/proactive_profile.py +196 -0
  158. package/python/mindbase/proactive_timing.py +28 -0
  159. package/python/mindbase/pyramid_stages.py +61 -0
  160. package/python/mindbase/quantum.py +132 -0
  161. package/python/mindbase/quantum_chaos.py +98 -0
  162. package/python/mindbase/reflection.py +68 -0
  163. package/python/mindbase/regulation_posture.py +50 -0
  164. package/python/mindbase/relationship_session.py +431 -0
  165. package/python/mindbase/relationship_suite.py +45 -0
  166. package/python/mindbase/relationships.py +70 -0
  167. package/python/mindbase/replay.py +22 -0
  168. package/python/mindbase/reply_diversity.py +53 -0
  169. package/python/mindbase/reply_gate.py +52 -0
  170. package/python/mindbase/rerank.py +41 -0
  171. package/python/mindbase/rerank_local.py +46 -0
  172. package/python/mindbase/ripple.py +187 -0
  173. package/python/mindbase/robust_search.py +68 -0
  174. package/python/mindbase/runtime.py +113 -0
  175. package/python/mindbase/scoped_identity.py +116 -0
  176. package/python/mindbase/self_insight.py +51 -0
  177. package/python/mindbase/self_model.py +116 -0
  178. package/python/mindbase/selftest.py +118 -0
  179. package/python/mindbase/semantic_memory.py +108 -0
  180. package/python/mindbase/session_store.py +175 -0
  181. package/python/mindbase/session_window.py +31 -0
  182. package/python/mindbase/shadow_eval.py +42 -0
  183. package/python/mindbase/sim_env.py +86 -0
  184. package/python/mindbase/social.py +138 -0
  185. package/python/mindbase/social_constraint.py +141 -0
  186. package/python/mindbase/social_feedback.py +95 -0
  187. package/python/mindbase/social_norm.py +225 -0
  188. package/python/mindbase/spiking_reservoir.py +204 -0
  189. package/python/mindbase/spontaneity.py +40 -0
  190. package/python/mindbase/story_loop.py +203 -0
  191. package/python/mindbase/survival_gauntlet.py +128 -0
  192. package/python/mindbase/symbolic.py +170 -0
  193. package/python/mindbase/teacher.py +244 -0
  194. package/python/mindbase/theory.py +42 -0
  195. package/python/mindbase/theory_of_mind.py +98 -0
  196. package/python/mindbase/translation_layer.py +80 -0
  197. package/python/mindbase/turn_eval.py +52 -0
  198. package/python/mindbase/types.py +78 -0
  199. package/python/mindbase/uncertainty_field.py +61 -0
  200. package/python/mindbase/user_loop.py +45 -0
  201. package/python/mindbase/user_profile.py +73 -0
  202. package/python/mindbase/value_model.py +64 -0
  203. package/python/mindbase/values.py +102 -0
  204. package/python/mindbase/vector_store.py +78 -0
  205. package/python/mindbase/viability.py +41 -0
  206. package/python/mindbase/virtual_body.py +90 -0
  207. package/python/mindbase/will.py +60 -0
  208. package/python/mindbase/working_memory.py +32 -0
  209. package/python/mindbase/workspace.py +55 -0
  210. package/ui/panel.css +13 -1
  211. package/ui/panel.html +26 -1
  212. package/ui/panel.js +58 -15
package/lib/index.js CHANGED
@@ -106,6 +106,27 @@ function normalizeEmbedding(value) {
106
106
  }
107
107
  }
108
108
 
109
+ export const RERANK_DEFAULTS = Object.freeze({
110
+ enabled: false,
111
+ base_url: 'https://api.jina.ai/v1',
112
+ model: 'jina-reranker-v2-base-multilingual',
113
+ api_key: '',
114
+ top_n: 20,
115
+ })
116
+
117
+ function normalizeRerank(value) {
118
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
119
+ const rawTop = Number(source.top_n)
120
+ const top = Number.isFinite(rawTop) ? Math.max(1, Math.min(100, Math.round(rawTop))) : RERANK_DEFAULTS.top_n
121
+ return {
122
+ enabled: source.enabled === true,
123
+ base_url: String(source.base_url || RERANK_DEFAULTS.base_url).trim().slice(0, 2000),
124
+ model: String(source.model || RERANK_DEFAULTS.model).trim().slice(0, 200),
125
+ api_key: typeof source.api_key === 'string' ? source.api_key.trim().slice(0, 4000) : '',
126
+ top_n: top,
127
+ }
128
+ }
129
+
109
130
  /** GET must never leak the plaintext key. */
110
131
  export function maskApiKey(key) {
111
132
  const text = String(key || '')
@@ -114,23 +135,14 @@ export function maskApiKey(key) {
114
135
  return text.slice(0, 2) + '••••••' + text.slice(-2)
115
136
  }
116
137
 
117
- function readStoredSettings(dataDir) {
118
- return readConfig(dataDir)
119
- }
120
-
121
- function writeSettingsFile(file, embedding, expression) {
122
- mkdirSync(dirname(file), { recursive: true })
123
- writeFileSync(file, JSON.stringify({ embedding, expression }, null, 2) + '\n', {
124
- encoding: 'utf8',
125
- mode: 0o600,
126
- })
127
- try { chmodSync(file, 0o600) } catch { /* Windows may ignore POSIX modes */ }
128
- }
129
-
130
138
  function maskedEmbedding(embedding) {
131
139
  return Object.assign({}, embedding, { api_key: maskApiKey(embedding.api_key) })
132
140
  }
133
141
 
142
+ function maskedRerank(rerank) {
143
+ return Object.assign({}, rerank, { api_key: maskApiKey(rerank.api_key) })
144
+ }
145
+
134
146
  function defaultEmbedding() {
135
147
  const embedding = emptyEmbedding()
136
148
  embedding.mode = 'auto'
@@ -144,16 +156,44 @@ function storedEmbedding(stored) {
144
156
  return embedding
145
157
  }
146
158
 
147
- /** GET/POST /api/biocore/settings: embedding + expression. */
159
+ /** Full config.json projection with defaults for all three sections. */
160
+ export function readFullConfig(dataDir) {
161
+ let doc = null
162
+ try {
163
+ doc = JSON.parse(readFileSync(join(String(dataDir || '').trim(), SETTINGS_FILE_NAME), 'utf8'))
164
+ } catch { doc = null }
165
+ const source = doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {}
166
+ return {
167
+ embedding: storedEmbedding(source),
168
+ rerank: normalizeRerank(source.rerank),
169
+ expression: normalizeExpression(source.expression),
170
+ sanitize_reply: source.sanitize_reply !== false,
171
+ }
172
+ }
173
+
174
+ function writeFullConfig(file, config) {
175
+ mkdirSync(dirname(file), { recursive: true })
176
+ writeFileSync(file, JSON.stringify({
177
+ embedding: config.embedding,
178
+ rerank: config.rerank,
179
+ expression: config.expression,
180
+ sanitize_reply: config.sanitize_reply !== false,
181
+ }, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
182
+ try { chmodSync(file, 0o600) } catch { /* Windows may ignore POSIX modes */ }
183
+ }
184
+
185
+ /** GET/POST /api/biocore/settings: embedding + rerank + expression. */
148
186
  export async function handleSettings(req, res, dataDir) {
149
187
  const paths = settingsPaths(dataDir)
150
188
  const method = String((req && req.method) || 'GET').toUpperCase()
151
189
  if (method === 'GET') {
152
- const stored = readStoredSettings(paths.dir)
190
+ const config = readFullConfig(paths.dir)
153
191
  return sendJson(res, 200, {
154
192
  ok: true,
155
- embedding: maskedEmbedding(storedEmbedding(stored)),
156
- expression: Object.assign({}, EXPRESSION_DEFAULTS, stored.expression || {}),
193
+ embedding: maskedEmbedding(config.embedding),
194
+ rerank: maskedRerank(config.rerank),
195
+ expression: config.expression,
196
+ sanitize_reply: config.sanitize_reply !== false,
157
197
  restart_required: false,
158
198
  })
159
199
  }
@@ -172,33 +212,136 @@ export async function handleSettings(req, res, dataDir) {
172
212
  if (body.confirm !== true) {
173
213
  return sendJson(res, 400, { ok: false, reason: 'confirm_required' })
174
214
  }
175
- const stored = readStoredSettings(paths.dir)
176
- let embedding = storedEmbedding(stored)
177
- let expression = Object.assign({}, EXPRESSION_DEFAULTS, stored.expression || {})
215
+ const config = readFullConfig(paths.dir)
178
216
  const hasEmbedding = body.embedding && typeof body.embedding === 'object'
217
+ const hasRerank = body.rerank && typeof body.rerank === 'object'
179
218
  const hasExpression = body.expression && typeof body.expression === 'object'
180
- if (!hasEmbedding && !hasExpression) {
219
+ const hasSanitize = typeof body.sanitize_reply === 'boolean'
220
+ if (!hasEmbedding && !hasRerank && !hasExpression && !hasSanitize) {
181
221
  return sendJson(res, 400, { ok: false, reason: 'no_settings' })
182
222
  }
183
223
  if (hasEmbedding) {
184
224
  const raw = body.embedding
185
- embedding = normalizeEmbedding(raw)
225
+ const embedding = normalizeEmbedding(raw)
186
226
  if (!embedding.mode) {
187
227
  return sendJson(res, 400, { ok: false, reason: 'bad_mode', allowed: Array.from(EMBEDDING_MODES) })
188
228
  }
189
- if (!('api_key' in raw)) embedding.api_key = storedEmbedding(stored).api_key
229
+ if (!('api_key' in raw) || String(raw.api_key || '').startsWith('••')) {
230
+ embedding.api_key = config.embedding.api_key
231
+ }
232
+ config.embedding = embedding
190
233
  }
191
- if (hasExpression) expression = normalizeExpression(body.expression)
192
- writeSettingsFile(paths.file, embedding, expression)
234
+ if (hasRerank) {
235
+ const raw = body.rerank
236
+ const rerank = normalizeRerank(raw)
237
+ if (!('api_key' in raw) || String(raw.api_key || '').startsWith('••')) {
238
+ rerank.api_key = config.rerank.api_key
239
+ }
240
+ config.rerank = rerank
241
+ }
242
+ if (hasExpression) config.expression = normalizeExpression(body.expression)
243
+ if (hasSanitize) config.sanitize_reply = body.sanitize_reply === true
244
+ writeFullConfig(paths.file, config)
193
245
  return sendJson(res, 200, {
194
246
  ok: true,
195
- embedding: maskedEmbedding(embedding),
196
- expression,
247
+ embedding: maskedEmbedding(config.embedding),
248
+ rerank: maskedRerank(config.rerank),
249
+ expression: config.expression,
250
+ sanitize_reply: config.sanitize_reply !== false,
197
251
  restart_required: true,
198
252
  hint: '重启 BioCore 生效',
199
253
  })
200
254
  }
201
255
 
256
+ function notConfigured(kind, detail) {
257
+ return { ok: false, kind, reason: 'not_configured', detail }
258
+ }
259
+
260
+ /** One minimal API call; never returns or logs the plaintext key. */
261
+ export async function testApiConnection(kind, config, options) {
262
+ const opts = options || {}
263
+ const fetchImpl = typeof opts.fetchImpl === 'function' ? opts.fetchImpl : globalThis.fetch
264
+ const timeoutMs = Number(opts.timeoutMs) || 6000
265
+ if (typeof fetchImpl !== 'function') return { ok: false, kind, reason: 'fetch_unavailable' }
266
+ let url = ''
267
+ let payload = null
268
+ if (kind === 'embedding') {
269
+ const embedding = config && config.embedding ? config.embedding : {}
270
+ if (embedding.mode === 'off' || embedding.mode === 'local') {
271
+ return notConfigured(kind, 'embedding mode is ' + embedding.mode)
272
+ }
273
+ if (!embedding.base_url || !embedding.model || !embedding.api_key) {
274
+ return notConfigured(kind, 'embedding base_url/model/api_key missing')
275
+ }
276
+ url = String(embedding.base_url).replace(/\/+$/, '') + '/embeddings'
277
+ payload = { model: embedding.model, input: 'ping' }
278
+ } else if (kind === 'rerank') {
279
+ const rerank = config && config.rerank ? config.rerank : {}
280
+ if (rerank.enabled !== true) return notConfigured(kind, 'rerank disabled')
281
+ if (!rerank.base_url || !rerank.model || !rerank.api_key) {
282
+ return notConfigured(kind, 'rerank base_url/model/api_key missing')
283
+ }
284
+ url = String(rerank.base_url).replace(/\/+$/, '') + '/rerank'
285
+ payload = {
286
+ model: rerank.model,
287
+ query: 'ping',
288
+ documents: ['ping'],
289
+ top_n: 1,
290
+ }
291
+ } else {
292
+ return { ok: false, kind: String(kind || ''), reason: 'bad_kind' }
293
+ }
294
+ const key = kind === 'embedding'
295
+ ? String(config.embedding.api_key || '')
296
+ : String(config.rerank.api_key || '')
297
+ const controller = typeof AbortController === 'function' ? new AbortController() : null
298
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null
299
+ const started = Date.now()
300
+ try {
301
+ const response = await fetchImpl(url, {
302
+ method: 'POST',
303
+ headers: { 'content-type': 'application/json', authorization: 'Bearer ' + key },
304
+ body: JSON.stringify(payload),
305
+ signal: controller ? controller.signal : undefined,
306
+ })
307
+ const status = Number(response && response.status) || 0
308
+ const elapsed = Date.now() - started
309
+ if (response && response.ok) {
310
+ return { ok: true, kind, status_code: status, elapsed_ms: elapsed }
311
+ }
312
+ return { ok: false, kind, reason: 'http_error', status_code: status, elapsed_ms: elapsed }
313
+ } catch (error) {
314
+ const name = error && error.name ? String(error.name) : ''
315
+ return {
316
+ ok: false,
317
+ kind,
318
+ reason: name === 'AbortError' ? 'timeout' : 'network_error',
319
+ elapsed_ms: Date.now() - started,
320
+ }
321
+ } finally {
322
+ if (timer) clearTimeout(timer)
323
+ }
324
+ }
325
+
326
+ /** POST /api/biocore/settings/test {kind:'embedding'|'rerank'}. */
327
+ export async function handleSettingsTest(req, res, options) {
328
+ const opts = options || {}
329
+ const dataDir = opts.dataDir
330
+ const method = String((req && req.method) || 'POST').toUpperCase()
331
+ if (method !== 'POST') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' })
332
+ let body = null
333
+ try { body = await readJsonBody(req) } catch (error) {
334
+ return sendJson(res, 400, { ok: false, reason: (error && error.reason) || 'bad_payload' })
335
+ }
336
+ const kind = String(body && body.kind || '').trim()
337
+ if (kind !== 'embedding' && kind !== 'rerank') {
338
+ return sendJson(res, 400, { ok: false, reason: 'bad_kind', allowed: ['embedding', 'rerank'] })
339
+ }
340
+ const config = readFullConfig(dataDir)
341
+ const result = await testApiConnection(kind, config, { fetchImpl: opts.fetchImpl })
342
+ return sendJson(res, 200, result)
343
+ }
344
+
202
345
  const PERSONA_PREVIEW_FIELDS = [
203
346
  ['E', '外向', 2],
204
347
  ['A', '宜人', 2],
@@ -692,6 +835,12 @@ export function apply(ctx, config) {
692
835
  path: SETTINGS_ROUTE_PATH,
693
836
  handler: (req, res) => handleSettings(req, res, dataDir),
694
837
  }))
838
+ // API connection test (embedding / rerank); never logs the key.
839
+ keep(webServer.register({
840
+ kind: 'exact',
841
+ path: SETTINGS_ROUTE_PATH + '/test',
842
+ handler: (req, res) => handleSettingsTest(req, res, { dataDir }),
843
+ }))
695
844
  })
696
845
  }
697
846
  } catch { /* headless */ }
@@ -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
@@ -118,6 +123,7 @@ export function turnEndPayload(options) {
118
123
  store: opts.store || defaultStoreDir(),
119
124
  capture: false,
120
125
  memoryEnabled: false,
126
+ sanitize_reply: opts.sanitizeReply !== false,
121
127
  diagnostics,
122
128
  }, diagnostics, {
123
129
  modules_trace: diagnostics.modules,
@@ -135,6 +141,9 @@ export function makeCoreSessionListener(options) {
135
141
  const getExpression = typeof opts.getExpression === 'function'
136
142
  ? opts.getExpression
137
143
  : () => readExpressionSettings(stateDir)
144
+ const getSanitizeReply = typeof opts.getSanitizeReply === 'function'
145
+ ? opts.getSanitizeReply
146
+ : () => readSanitizeReplySetting(stateDir)
138
147
  const store = opts.store || defaultStoreDir()
139
148
  const log = typeof opts.log === 'function' ? opts.log : (line) => {
140
149
  try { console.error(line) } catch { /* noop */ }
@@ -186,9 +195,11 @@ export function makeCoreSessionListener(options) {
186
195
  }).catch(() => null)
187
196
  return
188
197
  }
189
- // assistant/message: gate + trace via one turn_end call.
198
+ // assistant/message: sanitize -> gate + trace via one turn_end call.
190
199
  const original = assistantTextOf(event)
191
200
  if (!original) return
201
+ const sanitizeEnabled = sanitizeReplyEnabled(getSanitizeReply())
202
+ const cleaned = sanitizeEnabled ? sanitizeReply(original) : original
192
203
  const userText = lastUser.get(sessionId) || ''
193
204
  void (async () => {
194
205
  try {
@@ -198,11 +209,12 @@ export function makeCoreSessionListener(options) {
198
209
  const state = read && !read.error && !read.missing ? read.state : {}
199
210
  const payload = turnEndPayload({
200
211
  sessionId,
201
- assistantText: original,
212
+ assistantText: cleaned,
202
213
  userText,
203
214
  switches,
204
215
  store,
205
216
  state,
217
+ sanitizeReply: sanitizeEnabled,
206
218
  })
207
219
  const result = await runAction('turn_end', payload)
208
220
  if (!result || result.ok !== true) return
@@ -216,6 +228,9 @@ export function makeCoreSessionListener(options) {
216
228
  + ' blocked=' + (gate.blocked === true ? '1' : '0')
217
229
  + ' rule=' + stringId(gate.rule || '')
218
230
  + ' chars=' + original.length + '->' + gate.text.length)
231
+ } else if (cleaned !== original) {
232
+ // No gate rewrite: still write the sanitized text back.
233
+ applyGateText(event, cleaned)
219
234
  }
220
235
  if (result.trace_file) {
221
236
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-biocore",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "BioCore DSH UI: per-session state tab, standalone instrument panel, /api/biocore/state, session/event one_turn updates, /bio sync.",
6
6
  "main": "lib/index.js",
@@ -91,6 +91,11 @@ try:
91
91
  except Exception: # pragma: no cover
92
92
  _intero_core = None
93
93
 
94
+ try:
95
+ from biocore_runtime import rerank as _rerank_mod
96
+ except Exception: # pragma: no cover
97
+ _rerank_mod = None
98
+
94
99
  try: # self-layer modules (all pure; optional so a stripped checkout still runs)
95
100
  from biocore_model import x_directives as _x_directives
96
101
  from biocore_model import self_intent as _self_intent
@@ -634,6 +639,26 @@ def tool_call(name, args, store, session_id=None):
634
639
  row["score"] = score
635
640
  rows.append(row)
636
641
  rows.sort(key=lambda row: (-row["score"], -_num(row.get("ts"), 0.0)))
642
+ rerank_config = args.get("rerank_config")
643
+ if (_rerank_mod is not None and isinstance(rerank_config, dict)
644
+ and rerank_config.get("rerank_enabled")):
645
+ try:
646
+ reranked = _rerank_mod.rerank_documents(
647
+ query, [row.get("text", "") for row in rows], rerank_config)
648
+ if reranked:
649
+ seen = set()
650
+ ordered = []
651
+ for item in reranked:
652
+ index = int(item.get("index", -1))
653
+ if 0 <= index < len(rows) and index not in seen:
654
+ seen.add(index)
655
+ ordered.append(rows[index])
656
+ for index, row in enumerate(rows):
657
+ if index not in seen:
658
+ ordered.append(row)
659
+ rows = ordered
660
+ except Exception:
661
+ pass # rerank is optional; recall keeps the local order
637
662
  return {"ok": True, "count": min(limit, len(rows)),
638
663
  "results": rows[:limit]}
639
664
  target = str(args.get("id") or args.get("query") or "").strip()
@@ -1835,6 +1860,23 @@ def dispatch(action, payload=None):
1835
1860
  current = normalize_switches(data.get("switches"))
1836
1861
  current[key] = bool(data.get("value"))
1837
1862
  return {"ok": True, "switches": current}
1863
+ if action == "recall":
1864
+ query = str(data.get("query") or "").strip()
1865
+ documents = data.get("documents")
1866
+ if not isinstance(documents, list):
1867
+ documents = data.get("docs") if isinstance(data.get("docs"), list) else []
1868
+ fallback = [{"index": index,
1869
+ "document": str((item.get("text") or item.get("document"))
1870
+ if isinstance(item, dict) else (item or "")).strip()}
1871
+ for index, item in enumerate(documents)]
1872
+ if _rerank_mod is None:
1873
+ return {"ok": True, "reranked": False, "results": fallback,
1874
+ "reason": "unavailable"}
1875
+ config = data.get("config") if isinstance(data.get("config"), dict) else data
1876
+ result = _rerank_mod.rerank_documents(query, documents, config)
1877
+ if not result:
1878
+ return {"ok": True, "reranked": False, "results": fallback}
1879
+ return {"ok": True, "reranked": True, "results": result}
1838
1880
  if action == "trace":
1839
1881
  session_id = str(data.get("session_id") or "").strip()
1840
1882
  if not session_id:
@@ -0,0 +1,201 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Optional remote cross-encoder rerank (stdlib only, silent degradation).
3
+
4
+ Config keys (all optional, defaults shown):
5
+ rerank_enabled False
6
+ rerank_base_url ""
7
+ rerank_model ""
8
+ rerank_api_key ""
9
+ rerank_timeout 8.0 seconds
10
+ rerank_top_n 20
11
+
12
+ Two common wire protocols are tried automatically:
13
+ POST {base_url}/rerank {"model", "query", "documents", "top_n"}
14
+ POST {base_url}/v1/rerank same payload
15
+
16
+ A disabled call, missing config, timeout, non-2xx response, bad JSON or an
17
+ invalid result list returns None and records a short reason; callers keep the
18
+ original ordering. Only stdlib urllib is used.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ import math
25
+ import socket
26
+ import urllib.error
27
+ import urllib.request
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ DEFAULTS = {
32
+ "rerank_enabled": False,
33
+ "rerank_base_url": "",
34
+ "rerank_model": "",
35
+ "rerank_api_key": "",
36
+ "rerank_timeout": 8.0,
37
+ "rerank_top_n": 20,
38
+ }
39
+
40
+
41
+ def _num(value, default=0.0):
42
+ try:
43
+ number = float(value)
44
+ except Exception:
45
+ return float(default)
46
+ return number if math.isfinite(number) else float(default)
47
+
48
+
49
+ def _as_bool(value, default=False):
50
+ if isinstance(value, bool):
51
+ return value
52
+ if isinstance(value, (int, float)):
53
+ return bool(value)
54
+ if isinstance(value, str):
55
+ lowered = value.strip().lower()
56
+ if lowered in ("1", "true", "yes", "on"):
57
+ return True
58
+ if lowered in ("0", "false", "no", "off", ""):
59
+ return False
60
+ return bool(default)
61
+
62
+
63
+ def normalize_config(config=None):
64
+ """Merge caller config with defaults and clamp timeout/top_n."""
65
+ data = config if isinstance(config, dict) else {}
66
+ out = dict(DEFAULTS)
67
+ out["rerank_enabled"] = _as_bool(data.get("rerank_enabled"), False)
68
+ for key in ("rerank_base_url", "rerank_model", "rerank_api_key"):
69
+ out[key] = str(data.get(key) or "").strip()
70
+ timeout = _num(data.get("rerank_timeout"), DEFAULTS["rerank_timeout"])
71
+ out["rerank_timeout"] = max(0.1, min(60.0, timeout))
72
+ top_n = int(_num(data.get("rerank_top_n"), DEFAULTS["rerank_top_n"]))
73
+ out["rerank_top_n"] = max(1, min(1000, top_n))
74
+ return out
75
+
76
+
77
+ def _document_text(item):
78
+ if isinstance(item, dict):
79
+ return str(item.get("text") or item.get("document") or "").strip()
80
+ return str(item or "").strip()
81
+
82
+
83
+ def _candidate_urls(base_url):
84
+ base = str(base_url or "").strip().rstrip("/")
85
+ if not base:
86
+ return []
87
+ if base.endswith("/rerank"):
88
+ return [base]
89
+ urls = [base + "/rerank"]
90
+ if not base.endswith("/v1"):
91
+ urls.append(base + "/v1/rerank")
92
+ return urls
93
+
94
+
95
+ def _urllib_transport(url, payload, timeout, headers):
96
+ request = urllib.request.Request(
97
+ url, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
98
+ headers=headers, method="POST")
99
+ with urllib.request.urlopen(request, timeout=timeout) as response:
100
+ status = int(getattr(response, "status", 200) or 200)
101
+ body = response.read()
102
+ return status, body
103
+
104
+
105
+ def _notify(callback, reason):
106
+ try:
107
+ logger.info("[rerank] fallback: %s", reason)
108
+ except Exception:
109
+ pass
110
+ if callable(callback):
111
+ try:
112
+ callback(reason)
113
+ except Exception:
114
+ pass
115
+
116
+
117
+ def rerank_documents(query, documents, config=None, transport=None,
118
+ on_reason=None):
119
+ """Return [{index, document, relevance_score}] sorted/truncated, or None."""
120
+ cfg = normalize_config(config)
121
+ if not cfg["rerank_enabled"]:
122
+ _notify(on_reason, "disabled")
123
+ return None
124
+ if not cfg["rerank_base_url"] or not cfg["rerank_model"]:
125
+ _notify(on_reason, "missing_config")
126
+ return None
127
+ text = str(query or "").strip()
128
+ if not text:
129
+ _notify(on_reason, "empty_query")
130
+ return None
131
+ text_docs = []
132
+ for index, item in enumerate(documents or []):
133
+ content = _document_text(item)
134
+ if content:
135
+ text_docs.append((index, content))
136
+ if not text_docs:
137
+ _notify(on_reason, "empty_documents")
138
+ return None
139
+ top_n = min(cfg["rerank_top_n"], len(text_docs))
140
+ payload = {
141
+ "model": cfg["rerank_model"],
142
+ "query": text,
143
+ "documents": [content for _index, content in text_docs],
144
+ "top_n": top_n,
145
+ }
146
+ headers = {"Content-Type": "application/json"}
147
+ if cfg["rerank_api_key"]:
148
+ headers["Authorization"] = "Bearer " + cfg["rerank_api_key"]
149
+ call = transport or _urllib_transport
150
+ reason = "failed"
151
+ for url in _candidate_urls(cfg["rerank_base_url"]):
152
+ try:
153
+ status, body = call(url, payload, cfg["rerank_timeout"], headers)
154
+ except (TimeoutError, socket.timeout):
155
+ _notify(on_reason, "timeout")
156
+ return None
157
+ except urllib.error.HTTPError as error:
158
+ reason = "http_%s" % getattr(error, "code", "error")
159
+ continue
160
+ except Exception:
161
+ _notify(on_reason, "request_failed")
162
+ return None
163
+ try:
164
+ status = int(status)
165
+ except Exception:
166
+ status = 0
167
+ if status < 200 or status >= 300:
168
+ reason = "http_%s" % status
169
+ continue
170
+ try:
171
+ if isinstance(body, bytes):
172
+ body = body.decode("utf-8", "replace")
173
+ data = json.loads(body)
174
+ except Exception:
175
+ reason = "bad_json"
176
+ continue
177
+ results = data.get("results") if isinstance(data, dict) else None
178
+ if not isinstance(results, list):
179
+ reason = "bad_results"
180
+ continue
181
+ parsed = []
182
+ by_index = dict(text_docs)
183
+ for item in results:
184
+ if not isinstance(item, dict):
185
+ continue
186
+ try:
187
+ index = int(item.get("index"))
188
+ score = float(item.get("relevance_score"))
189
+ except Exception:
190
+ continue
191
+ if index not in by_index or not math.isfinite(score):
192
+ continue
193
+ parsed.append({"index": index, "document": by_index[index],
194
+ "relevance_score": round(score, 6)})
195
+ if not parsed:
196
+ reason = "empty_results"
197
+ continue
198
+ parsed.sort(key=lambda row: (-row["relevance_score"], row["index"]))
199
+ return parsed[:top_n]
200
+ _notify(on_reason, reason)
201
+ return None
@@ -0,0 +1,18 @@
1
+ """mindbase: a layered, paper-grounded base for a human-like agent.
2
+
3
+ Layers:
4
+ L0 substrate/noise -> noise.py
5
+ L1 drives/allostasis -> drives.py, clock.py
6
+ L2 affect/appraisal -> appraisal.py, affect.py
7
+ L3 attention/WM -> attention.py, working_memory.py
8
+ L4 memory -> memory.py
9
+ L5 social/persona/self/meta/autonomy -> social.py, persona.py, self_model.py, meta.py, autonomy.py
10
+ L6 expression -> (not implemented; LLM adapter later)
11
+ """
12
+
13
+ from .config import MindConfig
14
+ from .actions import Action
15
+ from .types import Event, Appraisal, Outcome, Feedback, LLMRequest
16
+ from .engine import MindBase
17
+
18
+ __all__ = ["MindConfig", "Event", "Appraisal", "Outcome", "LLMRequest", "Feedback", "Action", "MindBase"]